tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See CLI examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from dateutil.tz import tzlocal 41from time import sleep 42 43import re 44import json 45import requests 46import traceback as tb 47from typing import Union 48 49from multiprocessing import cpu_count 50from multiprocessing.pool import ThreadPool 51import pandas as pd 52 53from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 54from TradeRoutines import * # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module 55 56from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator 57from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 58 59import UniLogger as uLog # Logger for TKSBrokerAPI 60 61 62# --- Common technical parameters: 63 64PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 65uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 66uLogger.level = 10 # debug level by default for TKSBrokerAPI module 67uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 68 69__version__ = "1.5" # The "major.minor" version setup here, but build number define at the build-server only 70 71CPU_COUNT = cpu_count() # host's real CPU count 72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 73 74 75class TinkoffBrokerServer: 76 """ 77 This class implements methods to work with Tinkoff broker server. 78 79 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 80 81 About `token`: https://tinkoff.github.io/investAPI/token/ 82 """ 83 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 84 """ 85 Main class init. 86 87 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 88 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 89 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 90 :param useCache: use default cache file with raw data to use instead of `iList`. 91 True by default. Cache is auto-update if new day has come. 92 If you don't want to use cache and always updates raw data then set `useCache=False`. 93 :param defaultCache: path to default cache file. `dump.json` by default. 94 """ 95 if token is None or not token: 96 try: 97 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 98 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 99 100 except KeyError: 101 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 102 raise Exception("Token required") 103 104 else: 105 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 106 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 107 108 if accountId is None or not accountId: 109 try: 110 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 111 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 112 113 except KeyError: 114 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 115 116 else: 117 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 118 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 119 120 self.version = __version__ # duplicate here used TKSBrokerAPI main version 121 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 122 123 Latest version: https://pypi.org/project/tksbrokerapi/ 124 """ 125 126 self.aliases = TKS_TICKER_ALIASES 127 """Some aliases instead official tickers. 128 129 See also: `TKSEnums.TKS_TICKER_ALIASES` 130 """ 131 132 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 133 134 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 135 136 self.ticker = "" 137 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 138 139 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 140 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 141 142 See also: `SearchByTicker()`, `SearchInstruments()`. 143 """ 144 145 self.figi = "" 146 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 147 148 See also: `SearchByFIGI()`, `SearchInstruments()`. 149 """ 150 151 self.depth = 1 152 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 153 154 See also: `GetCurrentPrices()`. 155 """ 156 157 self.server = r"https://invest-public-api.tinkoff.ru/rest" 158 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 159 160 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 161 """ 162 163 uLogger.debug("Broker API server: {}".format(self.server)) 164 165 self.timeout = 15 166 """Server operations timeout in seconds. Default: `15`. 167 168 See also: `SendAPIRequest()`. 169 """ 170 171 self.headers = { 172 "Content-Type": "application/json", 173 "accept": "application/json", 174 "Authorization": "Bearer {}".format(self.token), 175 "x-app-name": "Tim55667757.TKSBrokerAPI", 176 } 177 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 178 179 See also: `SendAPIRequest()`. 180 """ 181 182 self.body = None 183 """Request body which send to broker server. Default: `None`. 184 185 See also: `SendAPIRequest()`. 186 """ 187 188 self.moreDebug = False 189 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 190 191 self.historyFile = None 192 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 193 194 See also: `History()`. 195 """ 196 197 self.htmlHistoryFile = "index.html" 198 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 199 200 See also: `ShowHistoryChart()`. 201 """ 202 203 self.instrumentsFile = "instruments.md" 204 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 205 206 See also: `ShowInstrumentsInfo()`. 207 """ 208 209 self.searchResultsFile = "search-results.md" 210 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 211 212 See also: `SearchInstruments()`. 213 """ 214 215 self.pricesFile = "prices.md" 216 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 217 218 See also: `GetListOfPrices()`. 219 """ 220 221 self.infoFile = "info.md" 222 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 223 224 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 225 """ 226 227 self.bondsXLSXFile = "ext-bonds.xlsx" 228 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 229 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 230 231 See also: `ExtendBondsData()`. 232 """ 233 234 self.calendarFile = "calendar.md" 235 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 236 237 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 238 239 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 240 """ 241 242 self.overviewFile = "overview.md" 243 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 244 245 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 246 """ 247 248 self.overviewDigestFile = "overview-digest.md" 249 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 250 251 See also: `Overview()` with parameter `details="digest"`. 252 """ 253 254 self.overviewPositionsFile = "overview-positions.md" 255 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 256 257 See also: `Overview()` with parameter `details="positions"`. 258 """ 259 260 self.overviewOrdersFile = "overview-orders.md" 261 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 262 263 See also: `Overview()` with parameter `details="orders"`. 264 """ 265 266 self.overviewAnalyticsFile = "overview-analytics.md" 267 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 268 269 See also: `Overview()` with parameter `details="analytics"`. 270 """ 271 272 self.overviewBondsCalendarFile = "overview-calendar.md" 273 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 274 275 See also: `Overview()` with parameter `details="calendar"`. 276 """ 277 278 self.reportFile = "deals.md" 279 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 280 281 See also: `Deals()`. 282 """ 283 284 self.withdrawalLimitsFile = "limits.md" 285 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 286 287 See also: `OverviewLimits()` and `RequestLimits()`. 288 """ 289 290 self.userInfoFile = "user-info.md" 291 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 292 293 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 294 """ 295 296 self.userAccountsFile = "accounts.md" 297 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 298 299 See also: `OverviewAccounts()`, `RequestAccounts()`. 300 """ 301 302 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 303 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 304 305 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 306 307 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 308 """ 309 310 self.iList = None # init iList for raw instruments data 311 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 312 313 See also: `Listing()`, `DumpInstruments()`. 314 """ 315 316 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 317 if useCache: 318 if os.path.exists(self.iListDumpFile): 319 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 320 curTime = datetime.now(tzutc()) 321 322 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 323 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 324 325 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 326 327 else: 328 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 329 330 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 331 os.path.abspath(self.iListDumpFile), 332 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 333 )) 334 335 else: 336 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 337 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 338 339 else: 340 self.iList = self.Listing() # request new raw instruments data from broker server 341 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 342 343 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 344 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 345 346 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 347 """ 348 349 def _ParseJSON(self, rawData="{}") -> dict: 350 """ 351 Parse JSON from response string. 352 353 :param rawData: this is a string with JSON-formatted text. 354 :return: JSON (dictionary), parsed from server response string. 355 """ 356 responseJSON = json.loads(rawData) if rawData else {} 357 358 if self.moreDebug: 359 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 360 361 return responseJSON 362 363 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 364 """ 365 Send GET or POST request to broker server and receive JSON object. 366 367 self.header: must be defining with dictionary of headers. 368 self.body: if define then used as request body. None by default. 369 self.timeout: global request timeout, 15 seconds by default. 370 :param url: url with REST request. 371 :param reqType: send "GET" or "POST" request. "GET" by default. 372 :param retry: how many times retry after first request if an 5xx server errors occurred. 373 :param pause: sleep time in seconds between retries. 374 :return: response JSON (dictionary) from broker. 375 """ 376 if reqType.upper() not in ("GET", "POST"): 377 uLogger.error("You can define request type: `GET` or `POST`!") 378 raise Exception("Incorrect value") 379 380 if self.moreDebug: 381 uLogger.debug("Request parameters:") 382 uLogger.debug(" - REST API URL: {}".format(url)) 383 uLogger.debug(" - request type: {}".format(reqType)) 384 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 385 uLogger.debug(" - body:\n{}".format(self.body)) 386 387 # fast hack to avoid all operations with some tickers/FIGI 388 responseJSON = {} 389 oK = True 390 for item in self.exclude: 391 if item in url: 392 if self.moreDebug: 393 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 394 395 oK = False 396 break 397 398 if oK: 399 counter = 0 400 response = None 401 errMsg = "" 402 403 while not response and counter <= retry: 404 if reqType == "GET": 405 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 406 407 if reqType == "POST": 408 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 409 410 if self.moreDebug: 411 uLogger.debug("Response:") 412 uLogger.debug(" - status code: {}".format(response.status_code)) 413 uLogger.debug(" - reason: {}".format(response.reason)) 414 uLogger.debug(" - body length: {}".format(len(response.text))) 415 uLogger.debug(" - headers:\n{}".format(response.headers)) 416 417 # Server returns some headers: 418 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 419 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 420 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 421 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 422 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 423 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 424 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 425 sleep(rateLimitWait) 426 427 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 428 if 400 <= response.status_code < 500: 429 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 430 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 431 432 if "code" in response.text and "message" in response.text: 433 msgDict = self._ParseJSON(rawData=response.text) 434 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 435 436 counter = retry + 1 # do not retry for 4xx errors 437 438 if 500 <= response.status_code < 600: 439 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 440 uLogger.debug(" - not oK, {}".format(errMsg)) 441 442 if "code" in response.text and "message" in response.text: 443 errMsgDict = self._ParseJSON(rawData=response.text) 444 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 445 446 counter += 1 447 448 if counter <= retry: 449 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 450 sleep(pause) 451 452 responseJSON = self._ParseJSON(rawData=response.text) 453 454 if errMsg: 455 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 456 uLogger.error(" - not oK, {}".format(errMsg)) 457 458 return responseJSON 459 460 def _IUpdater(self, iType: str) -> tuple: 461 """ 462 Request instrument by type from server. See available API methods for instruments: 463 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 464 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 465 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 466 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 467 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 468 469 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 470 :return: tuple with iType name and list of available instruments of current type for defined user token. 471 """ 472 result = [] 473 474 if iType in TKS_INSTRUMENTS: 475 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 476 477 # all instruments have the same body in API v2 requests: 478 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 479 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 480 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 481 482 return iType, result 483 484 def _IWrapper(self, kwargs): 485 """ 486 Wrapper runs instrument's update method `_IUpdater()`. 487 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 488 """ 489 return self._IUpdater(**kwargs) 490 491 def Listing(self) -> dict: 492 """ 493 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 494 495 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 496 """ 497 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 498 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 499 500 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 501 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 502 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 503 504 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 505 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 506 poolUpdater.close() 507 508 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 509 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 510 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 511 512 # calculate minimum price increment (step) for all instruments and set up instrument's type: 513 for iType in iList.keys(): 514 for ticker in iList[iType]: 515 iList[iType][ticker]["type"] = iType 516 517 if "minPriceIncrement" in iList[iType][ticker].keys(): 518 iList[iType][ticker]["step"] = NanoToFloat( 519 iList[iType][ticker]["minPriceIncrement"]["units"], 520 iList[iType][ticker]["minPriceIncrement"]["nano"], 521 ) 522 523 else: 524 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 525 526 return iList 527 528 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 529 """ 530 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 531 532 See also: `DumpInstruments()`, `Listing()`. 533 534 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 535 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 536 """ 537 if self.iListDumpFile is None or not self.iListDumpFile: 538 uLogger.error("Output name of dump file must be defined!") 539 raise Exception("Filename required") 540 541 if not self.iList or forceUpdate: 542 self.iList = self.Listing() 543 544 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 545 546 # Save as XLSX with separated sheets for every type of instruments: 547 with pd.ExcelWriter( 548 path=xlsxDumpFile, 549 date_format=TKS_DATE_FORMAT, 550 datetime_format=TKS_DATE_TIME_FORMAT, 551 mode="w", 552 ) as writer: 553 for iType in TKS_INSTRUMENTS: 554 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 555 df = df[sorted(df)] # sorted by column names 556 df = df.applymap( 557 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 558 na_action="ignore", 559 ) # converting numbers from nano-type to float in every cell 560 df.to_excel( 561 writer, 562 sheet_name=iType, 563 encoding="UTF-8", 564 freeze_panes=(1, 1), 565 ) # saving as XLSX-file with freeze first row and column as headers 566 567 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 568 569 def DumpInstruments(self, forceUpdate: bool = True) -> str: 570 """ 571 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 572 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 573 574 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 575 576 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 577 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 578 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 579 """ 580 if self.iListDumpFile is None or not self.iListDumpFile: 581 uLogger.error("Output name of dump file must be defined!") 582 raise Exception("Filename required") 583 584 if not self.iList or forceUpdate: 585 self.iList = self.Listing() 586 587 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 588 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 589 fH.write(jsonDump) 590 591 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 592 593 return jsonDump 594 595 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 596 """ 597 Show information about one instrument defined by json data and prints it in Markdown format. 598 599 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 600 601 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 602 :param show: if `True` then also printing information about instrument and its current price. 603 :return: multilines text in Markdown format with information about one instrument. 604 """ 605 splitLine = "| | |\n" 606 infoText = "" 607 608 if iJSON is not None and iJSON and isinstance(iJSON, dict): 609 info = [ 610 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 611 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 612 "| Parameters | Values |\n", 613 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 614 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 615 "| Full name: | {:<54} |\n".format(iJSON["name"]), 616 ] 617 618 if "sector" in iJSON.keys() and iJSON["sector"]: 619 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 620 621 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 622 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 623 624 info.extend([ 625 splitLine, 626 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 627 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 628 ]) 629 630 if "isin" in iJSON.keys() and iJSON["isin"]: 631 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 632 633 if "classCode" in iJSON.keys(): 634 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 635 636 info.extend([ 637 splitLine, 638 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 639 splitLine, 640 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 641 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 642 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 643 ]) 644 645 if iJSON["figi"]: 646 self.figi = iJSON["figi"] 647 iJSON = iJSON | self.RequestTradingStatus() 648 649 info.extend([ 650 splitLine, 651 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 652 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 653 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 654 ]) 655 656 info.append(splitLine) 657 658 if "type" in iJSON.keys() and iJSON["type"]: 659 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 660 661 if "shareType" in iJSON.keys() and iJSON["shareType"]: 662 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 663 664 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 665 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 666 667 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 668 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 669 670 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 671 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 672 673 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 674 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 675 676 if "focusType" in iJSON.keys() and iJSON["focusType"]: 677 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 678 679 if "assetType" in iJSON.keys() and iJSON["assetType"]: 680 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 681 682 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 683 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 684 685 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 686 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 687 688 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 689 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 690 691 if "currency" in iJSON.keys(): 692 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 693 694 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 695 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 696 697 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 698 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 699 700 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 701 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 702 703 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 704 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 705 706 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 707 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 708 709 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 710 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 711 712 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 713 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 714 715 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 716 info.append("| Perpetual bond: | Yes |\n") 717 718 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 719 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 720 721 iExt = None 722 if iJSON["type"] == "Bonds": 723 info.extend([ 724 splitLine, 725 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 726 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 727 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 728 iJSON["nominal"]["currency"], 729 )), 730 ]) 731 732 if "floatingCouponFlag" in iJSON.keys(): 733 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 734 735 if "amortizationFlag" in iJSON.keys(): 736 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 737 738 info.append(splitLine) 739 740 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 741 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 742 743 if iJSON["figi"]: 744 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 745 746 info.extend([ 747 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 748 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 749 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 750 ]) 751 752 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 753 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 754 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 755 iJSON["aciValue"]["currency"] 756 ))) 757 758 if "currentPrice" in iJSON.keys(): 759 info.append(splitLine) 760 761 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 762 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 763 764 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 765 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 766 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 767 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 768 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 769 770 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 771 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 772 773 info.extend([ 774 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 775 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 776 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 777 )), 778 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 779 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 780 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 781 )), 782 "| Changes between last deal price and last close | {:<54} |\n".format( 783 "{:.2f}%{}".format( 784 iJSON["currentPrice"]["changes"], 785 " ({}{:.2f} {})".format( 786 "+" if bondChangesDelta > 0 else "", 787 bondChangesDelta, 788 aciCurrency 789 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 790 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 791 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 792 currency 793 ), 794 ) 795 ), 796 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 797 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 798 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 799 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 800 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 801 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 802 )), 803 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 804 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 805 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 806 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 807 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 808 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 809 )), 810 ]) 811 812 if "lot" in iJSON.keys(): 813 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 814 815 if "step" in iJSON.keys() and iJSON["step"] != 0: 816 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 817 818 # Add bond payment calendar: 819 if iJSON["type"] == "Bonds": 820 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 821 info.extend(["\n", strCalendar]) 822 823 infoText += "".join(info) 824 825 if show: 826 uLogger.info("{}".format(infoText)) 827 828 else: 829 uLogger.debug("{}".format(infoText)) 830 831 if self.infoFile is not None: 832 with open(self.infoFile, "w", encoding="UTF-8") as fH: 833 fH.write(infoText) 834 835 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 836 837 return infoText 838 839 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 840 """ 841 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 842 843 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 844 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 845 :return: JSON formatted data with information about instrument. 846 """ 847 tickerJSON = {} 848 if self.moreDebug: 849 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 850 851 if not self.ticker: 852 uLogger.warning("self.ticker variable is not be empty!") 853 854 else: 855 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 856 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 857 raise Exception("Instrument not allowed") 858 859 if not self.iList: 860 self.iList = self.Listing() 861 862 if self.ticker in self.iList["Shares"].keys(): 863 tickerJSON = self.iList["Shares"][self.ticker] 864 if self.moreDebug: 865 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 866 867 elif self.ticker in self.iList["Currencies"].keys(): 868 tickerJSON = self.iList["Currencies"][self.ticker] 869 if self.moreDebug: 870 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 871 872 elif self.ticker in self.iList["Bonds"].keys(): 873 tickerJSON = self.iList["Bonds"][self.ticker] 874 if self.moreDebug: 875 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 876 877 elif self.ticker in self.iList["Etfs"].keys(): 878 tickerJSON = self.iList["Etfs"][self.ticker] 879 if self.moreDebug: 880 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 881 882 elif self.ticker in self.iList["Futures"].keys(): 883 tickerJSON = self.iList["Futures"][self.ticker] 884 if self.moreDebug: 885 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 886 887 if tickerJSON: 888 self.figi = tickerJSON["figi"] 889 890 if requestPrice: 891 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 892 893 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 894 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 895 896 else: 897 tickerJSON["currentPrice"]["changes"] = 0 898 899 if show: 900 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 901 902 else: 903 if show: 904 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 905 906 return tickerJSON 907 908 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 909 """ 910 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 911 912 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 913 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 914 :return: JSON formatted data with information about instrument. 915 """ 916 figiJSON = {} 917 if self.moreDebug: 918 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 919 920 if not self.figi: 921 uLogger.warning("self.figi variable is not be empty!") 922 923 else: 924 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 925 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 926 raise Exception("Instrument not allowed") 927 928 if not self.iList: 929 self.iList = self.Listing() 930 931 for item in self.iList["Shares"].keys(): 932 if self.figi == self.iList["Shares"][item]["figi"]: 933 figiJSON = self.iList["Shares"][item] 934 935 if self.moreDebug: 936 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 937 938 break 939 940 if not figiJSON: 941 for item in self.iList["Currencies"].keys(): 942 if self.figi == self.iList["Currencies"][item]["figi"]: 943 figiJSON = self.iList["Currencies"][item] 944 945 if self.moreDebug: 946 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 947 948 break 949 950 if not figiJSON: 951 for item in self.iList["Bonds"].keys(): 952 if self.figi == self.iList["Bonds"][item]["figi"]: 953 figiJSON = self.iList["Bonds"][item] 954 955 if self.moreDebug: 956 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 957 958 break 959 960 if not figiJSON: 961 for item in self.iList["Etfs"].keys(): 962 if self.figi == self.iList["Etfs"][item]["figi"]: 963 figiJSON = self.iList["Etfs"][item] 964 965 if self.moreDebug: 966 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 967 968 break 969 970 if not figiJSON: 971 for item in self.iList["Futures"].keys(): 972 if self.figi == self.iList["Futures"][item]["figi"]: 973 figiJSON = self.iList["Futures"][item] 974 975 if self.moreDebug: 976 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 977 978 break 979 980 if figiJSON: 981 self.figi = figiJSON["figi"] 982 self.ticker = figiJSON["ticker"] 983 984 if requestPrice: 985 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 986 987 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 988 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 989 990 else: 991 figiJSON["currentPrice"]["changes"] = 0 992 993 if show: 994 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 995 996 else: 997 if show: 998 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 999 1000 return figiJSON 1001 1002 def GetCurrentPrices(self, show: bool = True) -> dict: 1003 """ 1004 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1005 `{"buy": [{"price": 1243.8, "quantity": 193}, 1006 {"price": 1244.0, "quantity": 168}, 1007 {"price": 1244.8, "quantity": 5}, 1008 {"price": 1245.0, "quantity": 61}, 1009 {"price": 1245.4, "quantity": 60}], 1010 "sell": [{"price": 1243.6, "quantity": 8}, 1011 {"price": 1242.6, "quantity": 10}, 1012 {"price": 1242.4, "quantity": 18}, 1013 {"price": 1242.2, "quantity": 50}, 1014 {"price": 1242.0, "quantity": 113}], 1015 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1016 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1017 - sell: list of dicts with Buyers prices, 1018 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1019 - quantity: volume value by current price in lots, 1020 - limitUp: current trade session limit price, maximum, 1021 - limitDown: current trade session limit price, minimum, 1022 - lastPrice: last deal price of the instrument, 1023 - closePrice: previous trade session close price of the instrument. 1024 1025 See also: `SearchByTicker()` and `SearchByFIGI()`. 1026 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1027 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1028 1029 :param show: if `True` then print DOM to log and console. 1030 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1031 If an error occurred then returns an empty record: 1032 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1033 """ 1034 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1035 1036 if self.depth < 1: 1037 uLogger.error("Depth of Market (DOM) must be >=1!") 1038 raise Exception("Incorrect value") 1039 1040 if not (self.ticker or self.figi): 1041 uLogger.error("self.ticker or self.figi variables must be defined!") 1042 raise Exception("Ticker or FIGI required") 1043 1044 if self.ticker and not self.figi: 1045 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1046 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1047 1048 if not self.ticker and self.figi: 1049 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1050 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1051 1052 if not self.figi: 1053 uLogger.error("FIGI is not defined!") 1054 raise Exception("Ticker or FIGI required") 1055 1056 else: 1057 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1058 1059 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1060 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1061 self.body = str({"figi": self.figi, "depth": self.depth}) 1062 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1063 1064 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1065 # list of dicts with sellers orders: 1066 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1067 1068 # list of dicts with buyers orders: 1069 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1070 1071 # max price of instrument at this time: 1072 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1073 1074 # min price of instrument at this time: 1075 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1076 1077 # last price of deal with instrument: 1078 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1079 1080 # last close price of instrument: 1081 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1082 1083 else: 1084 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1085 uLogger.debug("Server response: {}".format(pricesResponse)) 1086 1087 if show: 1088 if prices["buy"] or prices["sell"]: 1089 info = [ 1090 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1091 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1092 self.ticker, 1093 self.figi, 1094 self.depth, 1095 ), 1096 "-" * 60, "\n", 1097 " Orders of Buyers | Orders of Sellers\n", 1098 "-" * 60, "\n", 1099 " Sell prices (volumes) | Buy prices (volumes)\n", 1100 "-" * 60, "\n", 1101 ] 1102 1103 if not prices["buy"]: 1104 info.append(" | No orders!\n") 1105 sumBuy = 0 1106 1107 else: 1108 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1109 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1110 for item in maxMinSorted: 1111 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1112 1113 if not prices["sell"]: 1114 info.append("No orders! |\n") 1115 sumSell = 0 1116 1117 else: 1118 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1119 for item in prices["sell"]: 1120 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1121 1122 info.extend([ 1123 "-" * 60, "\n", 1124 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1125 "-" * 60, "\n", 1126 ]) 1127 1128 infoText = "".join(info) 1129 1130 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1131 1132 else: 1133 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1134 1135 return prices 1136 1137 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1138 """ 1139 This method get and show information about all available broker instruments for current user account. 1140 If `instrumentsFile` string is not empty then also save information to this file. 1141 1142 :param show: if `True` then print results to console, if `False` — print only to file. 1143 :return: multi-lines string with all available broker instruments 1144 """ 1145 if not self.iList: 1146 self.iList = self.Listing() 1147 1148 info = [ 1149 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1150 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1151 ] 1152 1153 # add instruments count by type: 1154 for iType in self.iList.keys(): 1155 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1156 1157 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1158 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1159 1160 # generating info tables with all instruments by type: 1161 for iType in self.iList.keys(): 1162 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1163 1164 for instrument in self.iList[iType].keys(): 1165 iName = self.iList[iType][instrument]["name"] # instrument's name 1166 if len(iName) > 57: 1167 iName = "{}...".format(iName[:54]) # right trim for a long string 1168 1169 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1170 self.iList[iType][instrument]["ticker"], 1171 iName, 1172 self.iList[iType][instrument]["figi"], 1173 self.iList[iType][instrument]["currency"], 1174 self.iList[iType][instrument]["lot"], 1175 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1176 )) 1177 1178 infoText = "".join(info) 1179 1180 if show: 1181 uLogger.info(infoText) 1182 1183 if self.instrumentsFile: 1184 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1185 fH.write(infoText) 1186 1187 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1188 1189 return infoText 1190 1191 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1192 """ 1193 This method search and show information about instruments by part of its ticker, FIGI or name. 1194 If `searchResultsFile` string is not empty then also save information to this file. 1195 1196 :param pattern: string with part of ticker, FIGI or instrument's name. 1197 :param show: if `True` then print results to console, if `False` — return list of result only. 1198 :return: list of dictionaries with all found instruments. 1199 """ 1200 if not self.iList: 1201 self.iList = self.Listing() 1202 1203 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1204 compiledPattern = re.compile(pattern, re.IGNORECASE) 1205 1206 for iType in self.iList: 1207 for instrument in self.iList[iType].values(): 1208 searchResult = compiledPattern.search(" ".join( 1209 [instrument["ticker"], instrument["figi"], instrument["name"]] 1210 )) 1211 1212 if searchResult: 1213 searchResults[iType][instrument["ticker"]] = instrument 1214 1215 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1216 info = [ 1217 "# Search results\n\n", 1218 "* **Search pattern:** [{}]\n".format(pattern), 1219 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1220 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1221 ] 1222 infoShort = info[:] 1223 1224 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1225 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1226 skippedLine = "| ... | ... | ... | ... |\n" 1227 1228 if resultsLen == 0: 1229 info.append("\nNo results\n") 1230 infoShort.append("\nNo results\n") 1231 uLogger.warning("No results. Try changing your search pattern.") 1232 1233 else: 1234 for iType in searchResults: 1235 iTypeValuesCount = len(searchResults[iType].values()) 1236 if iTypeValuesCount > 0: 1237 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1238 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1239 1240 for instrument in searchResults[iType].values(): 1241 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1242 instrument["type"], 1243 instrument["ticker"], 1244 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1245 instrument["figi"], 1246 )) 1247 1248 if iTypeValuesCount <= 5: 1249 infoShort.extend(info[-iTypeValuesCount:]) 1250 1251 else: 1252 infoShort.extend(info[-5:]) 1253 infoShort.append(skippedLine) 1254 1255 infoText = "".join(info) 1256 infoTextShort = "".join(infoShort) 1257 1258 if show: 1259 uLogger.info(infoTextShort) 1260 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1261 1262 if self.searchResultsFile: 1263 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1264 fH.write(infoText) 1265 1266 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1267 1268 return searchResults 1269 1270 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1271 """ 1272 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1273 1274 :param instruments: list of strings with tickers or FIGIs. 1275 :return: list with unique instrument FIGIs only. 1276 """ 1277 requestedInstruments = [] 1278 for iName in instruments: 1279 if iName not in self.aliases.keys(): 1280 if iName not in requestedInstruments: 1281 requestedInstruments.append(iName) 1282 1283 else: 1284 if iName not in requestedInstruments: 1285 if self.aliases[iName] not in requestedInstruments: 1286 requestedInstruments.append(self.aliases[iName]) 1287 1288 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1289 1290 onlyUniqueFIGIs = [] 1291 for iName in requestedInstruments: 1292 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1293 continue 1294 1295 self.ticker = iName 1296 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1297 1298 if not iData: 1299 self.ticker = "" 1300 self.figi = iName 1301 1302 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1303 1304 if not iData: 1305 self.figi = "" 1306 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1307 1308 if iData and iData["figi"] not in onlyUniqueFIGIs: 1309 onlyUniqueFIGIs.append(iData["figi"]) 1310 1311 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1312 1313 return onlyUniqueFIGIs 1314 1315 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1316 """ 1317 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1318 1319 See limits: https://tinkoff.github.io/investAPI/limits/ 1320 1321 If `pricesFile` string is not empty then also save information to this file. 1322 1323 :param instruments: list of strings with tickers or FIGIs. 1324 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1325 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1326 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1327 """ 1328 if instruments is None or not instruments: 1329 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1330 raise Exception("Ticker or FIGI required") 1331 1332 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1333 1334 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1335 1336 iList = [] # trying to get info and current prices about all unique instruments: 1337 for self.figi in onlyUniqueFIGIs: 1338 iData = self.SearchByFIGI(requestPrice=True) 1339 iList.append(iData) 1340 1341 self.ShowListOfPrices(iList, show) 1342 1343 return iList 1344 1345 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1346 """ 1347 Show table contains current prices of given instruments. 1348 1349 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1350 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1351 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1352 :return: multilines text in Markdown format as a table contains current prices. 1353 """ 1354 infoText = "" 1355 1356 if show or self.pricesFile: 1357 info = [ 1358 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1359 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1360 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1361 ] 1362 1363 for item in iList: 1364 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1365 item["ticker"], 1366 item["figi"], 1367 item["type"], 1368 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1369 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1370 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1371 "{} / {}".format( 1372 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1373 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1374 ), 1375 "{} / {}".format( 1376 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1377 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1378 ), 1379 item["currency"], 1380 )) 1381 1382 infoText = "".join(info) 1383 1384 if show: 1385 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1386 1387 if self.pricesFile: 1388 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1389 fH.write(infoText) 1390 1391 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1392 1393 return infoText 1394 1395 def RequestTradingStatus(self) -> dict: 1396 """ 1397 Requesting trading status for the instrument defined by `figi` variable. 1398 1399 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1400 1401 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1402 1403 :return: dictionary with trading status attributes. Response example: 1404 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1405 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1406 """ 1407 if self.figi is None or not self.figi: 1408 uLogger.error("Variable `figi` must be defined for using this method!") 1409 raise Exception("FIGI required") 1410 1411 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1412 1413 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1414 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1415 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1416 1417 if self.moreDebug: 1418 uLogger.debug("Records about current trading status successfully received") 1419 1420 return tradingStatus 1421 1422 def RequestPortfolio(self) -> dict: 1423 """ 1424 Requesting actual user's portfolio for current `accountId`. 1425 1426 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1427 1428 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1429 1430 :return: dictionary with user's portfolio. 1431 """ 1432 if self.accountId is None or not self.accountId: 1433 uLogger.error("Variable `accountId` must be defined for using this method!") 1434 raise Exception("Account ID required") 1435 1436 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1437 1438 self.body = str({"accountId": self.accountId}) 1439 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1440 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1441 1442 if self.moreDebug: 1443 uLogger.debug("Records about user's portfolio successfully received") 1444 1445 return rawPortfolio 1446 1447 def RequestPositions(self) -> dict: 1448 """ 1449 Requesting open positions by currencies and instruments for current `accountId`. 1450 1451 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1452 1453 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1454 1455 :return: dictionary with open positions by instruments. 1456 """ 1457 if self.accountId is None or not self.accountId: 1458 uLogger.error("Variable `accountId` must be defined for using this method!") 1459 raise Exception("Account ID required") 1460 1461 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1462 1463 self.body = str({"accountId": self.accountId}) 1464 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1465 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1466 1467 if self.moreDebug: 1468 uLogger.debug("Records about current open positions successfully received") 1469 1470 return rawPositions 1471 1472 def RequestPendingOrders(self) -> list: 1473 """ 1474 Requesting current actual pending limit orders for current `accountId`. 1475 1476 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1477 1478 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1479 1480 :return: list of dictionaries with pending limit orders. 1481 """ 1482 if self.accountId is None or not self.accountId: 1483 uLogger.error("Variable `accountId` must be defined for using this method!") 1484 raise Exception("Account ID required") 1485 1486 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1487 1488 self.body = str({"accountId": self.accountId}) 1489 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1490 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1491 1492 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1493 1494 return rawOrders 1495 1496 def RequestStopOrders(self) -> list: 1497 """ 1498 Requesting current actual stop orders for current `accountId`. 1499 1500 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1501 1502 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1503 1504 :return: list of dictionaries with stop orders. 1505 """ 1506 if self.accountId is None or not self.accountId: 1507 uLogger.error("Variable `accountId` must be defined for using this method!") 1508 raise Exception("Account ID required") 1509 1510 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1511 1512 self.body = str({"accountId": self.accountId}) 1513 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1514 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1515 1516 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1517 1518 return rawStopOrders 1519 1520 def Overview(self, show: bool = False, details: str = "full") -> dict: 1521 """ 1522 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1523 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1524 and `overviewBondsCalendarFile` are defined then also save information to file. 1525 1526 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1527 many requests about the state of the portfolio, and then, based on the received data, a large number 1528 of calculation and statistics are collected. 1529 1530 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1531 :param details: how detailed should the information be? 1532 - `full` — shows full available information about portfolio status (by default), 1533 - `positions` — shows only open positions, 1534 - `orders` — shows only sections of open limits and stop orders. 1535 - `digest` — show a short digest of the portfolio status, 1536 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1537 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1538 :return: dictionary with client's raw portfolio and some statistics. 1539 """ 1540 if self.accountId is None or not self.accountId: 1541 uLogger.error("Variable `accountId` must be defined for using this method!") 1542 raise Exception("Account ID required") 1543 1544 view = { 1545 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1546 "headers": {}, # list of dictionaries, response headers without "positions" section 1547 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1548 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1549 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1550 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1551 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1552 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1553 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1554 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1555 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1556 }, 1557 "stat": { # --- some statistics calculated using "raw" sections: 1558 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1559 "availableRUB": 0., # available rubles (without other currencies) 1560 "blockedRUB": 0., # blocked sum in Russian Rouble 1561 "totalChangesRUB": 0., # changes for all open trades in RUB 1562 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1563 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1564 "sharesCostRUB": 0., # costs of all shares in RUB 1565 "bondsCostRUB": 0., # costs of all bonds in RUB 1566 "etfsCostRUB": 0., # costs of all etfs in RUB 1567 "futuresCostRUB": 0., # costs of all futures in RUB 1568 "Currencies": [], # list of dictionaries of all currencies statistics 1569 "Shares": [], # list of dictionaries of all shares statistics 1570 "Bonds": [], # list of dictionaries of all bonds statistics 1571 "Etfs": [], # list of dictionaries of all etfs statistics 1572 "Futures": [], # list of dictionaries of all futures statistics 1573 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1574 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1575 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1576 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1577 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1578 }, 1579 "analytics": { # --- some analytics of portfolio: 1580 "distrByAssets": {}, # portfolio distribution by assets 1581 "distrByCompanies": {}, # portfolio distribution by companies 1582 "distrBySectors": {}, # portfolio distribution by sectors 1583 "distrByCurrencies": {}, # portfolio distribution by currencies 1584 "distrByCountries": {}, # portfolio distribution by countries 1585 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1586 } 1587 } 1588 1589 details = details.lower() 1590 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1591 if details not in availableDetails: 1592 details = "full" 1593 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1594 1595 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1596 1597 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1598 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1599 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1600 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1601 1602 # save response headers without "positions" section: 1603 for key in portfolioResponse.keys(): 1604 if key != "positions": 1605 view["raw"]["headers"][key] = portfolioResponse[key] 1606 1607 else: 1608 continue 1609 1610 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1611 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1612 for item in portfolioResponse["positions"]: 1613 if item["instrumentType"] == "currency": 1614 self.figi = item["figi"] 1615 curr = self.SearchByFIGI(requestPrice=False) 1616 1617 # current price of currency in RUB: 1618 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1619 "name": curr["name"], 1620 "currentPrice": NanoToFloat( 1621 item["currentPrice"]["units"], 1622 item["currentPrice"]["nano"] 1623 ), 1624 } 1625 1626 view["raw"]["Currencies"].append(item) 1627 1628 elif item["instrumentType"] == "share": 1629 view["raw"]["Shares"].append(item) 1630 1631 elif item["instrumentType"] == "bond": 1632 view["raw"]["Bonds"].append(item) 1633 1634 elif item["instrumentType"] == "etf": 1635 view["raw"]["Etfs"].append(item) 1636 1637 elif item["instrumentType"] == "futures": 1638 view["raw"]["Futures"].append(item) 1639 1640 else: 1641 continue 1642 1643 # how many volume of currencies (by ISO currency name) are blocked: 1644 for item in view["raw"]["positions"]["blocked"]: 1645 blocked = NanoToFloat(item["units"], item["nano"]) 1646 if blocked > 0: 1647 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1648 1649 # how many volume of instruments (by FIGI) are blocked: 1650 for item in view["raw"]["positions"]["securities"]: 1651 blocked = int(item["blocked"]) 1652 if blocked > 0: 1653 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1654 1655 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1656 1657 if "rub" in allBlocked.keys(): 1658 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1659 1660 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1661 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1662 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1663 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1664 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1665 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1666 view["stat"]["portfolioCostRUB"] = sum([ 1667 view["stat"]["allCurrenciesCostRUB"], 1668 view["stat"]["sharesCostRUB"], 1669 view["stat"]["bondsCostRUB"], 1670 view["stat"]["etfsCostRUB"], 1671 view["stat"]["futuresCostRUB"], 1672 ]) 1673 1674 # --- calculating some portfolio statistics: 1675 byComp = {} # distribution by companies 1676 bySect = {} # distribution by sectors 1677 byCurr = {} # distribution by currencies (include RUB) 1678 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1679 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1680 1681 for item in portfolioResponse["positions"]: 1682 self.figi = item["figi"] 1683 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1684 1685 if instrument: 1686 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1687 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1688 1689 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1690 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1691 1692 else: 1693 blocked = 0 1694 1695 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1696 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1697 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1698 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1699 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1700 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1701 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1702 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1703 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1704 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1705 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1706 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1707 1708 statData = { 1709 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1710 "ticker": instrument["ticker"], # ticker by FIGI 1711 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1712 "volume": volume, # available volume of instrument 1713 "lots": lots, # volume in lots of instrument 1714 "direction": direction, # direction of an instrument's position: short or long 1715 "blocked": blocked, # blocked volume of currency or instrument 1716 "currentPrice": curPrice, # current instrument's price in basic asset 1717 "average": average, # current average position price 1718 "cost": cost, # current cost of all volume of instrument in basic asset 1719 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1720 "costRUB": costRUB, # cost of instrument in ruble 1721 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1722 "profit": profit, # expected profit at current moment 1723 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1724 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1725 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1726 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1727 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1728 "step": instrument["step"], # minimum price increment 1729 } 1730 1731 # adding distribution by unique countries: 1732 if statData["country"] not in byCountry.keys(): 1733 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1734 1735 else: 1736 byCountry[statData["country"]]["cost"] += costRUB 1737 byCountry[statData["country"]]["percent"] += percentCostRUB 1738 1739 if item["instrumentType"] != "currency": 1740 # adding distribution by unique companies: 1741 if statData["name"]: 1742 if statData["name"] not in byComp.keys(): 1743 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1744 1745 else: 1746 byComp[statData["name"]]["cost"] += costRUB 1747 byComp[statData["name"]]["percent"] += percentCostRUB 1748 1749 # adding distribution by unique sectors: 1750 if statData["sector"] not in bySect.keys(): 1751 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1752 1753 else: 1754 bySect[statData["sector"]]["cost"] += costRUB 1755 bySect[statData["sector"]]["percent"] += percentCostRUB 1756 1757 # adding distribution by unique currencies: 1758 if currency not in byCurr.keys(): 1759 byCurr[currency] = { 1760 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1761 "cost": costRUB, 1762 "percent": percentCostRUB 1763 } 1764 1765 else: 1766 byCurr[currency]["cost"] += costRUB 1767 byCurr[currency]["percent"] += percentCostRUB 1768 1769 # saving statistics for every instrument: 1770 if item["instrumentType"] == "currency": 1771 view["stat"]["Currencies"].append(statData) 1772 1773 # update dict with free funds for trading (total - blocked) by currencies 1774 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1775 view["stat"]["funds"][currency] = { 1776 "total": volume, 1777 "totalCostRUB": costRUB, # total volume cost in rubles 1778 "free": volume - blocked, 1779 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1780 } 1781 1782 elif item["instrumentType"] == "share": 1783 view["stat"]["Shares"].append(statData) 1784 1785 elif item["instrumentType"] == "bond": 1786 view["stat"]["Bonds"].append(statData) 1787 1788 elif item["instrumentType"] == "etf": 1789 view["stat"]["Etfs"].append(statData) 1790 1791 elif item["instrumentType"] == "Futures": 1792 view["stat"]["Futures"].append(statData) 1793 1794 else: 1795 continue 1796 1797 # total changes in Russian Ruble: 1798 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1799 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1800 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1801 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1802 view["stat"]["funds"]["rub"] = { 1803 "total": view["stat"]["availableRUB"], 1804 "totalCostRUB": view["stat"]["availableRUB"], 1805 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1806 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1807 } 1808 1809 # --- pending limit orders sector data: 1810 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1811 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1812 1813 for item in view["raw"]["orders"]: 1814 self.figi = item["figi"] 1815 1816 if item["figi"] not in uniquePendingOrdersFIGIs: 1817 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1818 1819 uniquePendingOrdersFIGIs.append(item["figi"]) 1820 uniquePendingOrders[item["figi"]] = instrument 1821 1822 else: 1823 instrument = uniquePendingOrders[item["figi"]] 1824 1825 if instrument: 1826 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1827 orderType = TKS_ORDER_TYPES[item["orderType"]] 1828 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1829 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1830 1831 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1832 if item["direction"] == "ORDER_DIRECTION_BUY": 1833 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1834 1835 else: 1836 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1837 1838 # requested price for order execution: 1839 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1840 1841 # necessary changes in percent to reach target from current price: 1842 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1843 1844 view["stat"]["orders"].append({ 1845 "orderID": item["orderId"], # orderId number parameter of current order 1846 "figi": item["figi"], # FIGI identification 1847 "ticker": instrument["ticker"], # ticker name by FIGI 1848 "lotsRequested": item["lotsRequested"], # requested lots value 1849 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1850 "currentPrice": lastPrice, # current instrument's price for defined action 1851 "targetPrice": target, # requested price for order execution in base currency 1852 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1853 "percentChanges": changes, # changes in percent to target from current price 1854 "currency": item["currency"], # instrument's currency name 1855 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1856 "type": orderType, # type of order from TKS_ORDER_TYPES 1857 "status": orderState, # order status from TKS_ORDER_STATES 1858 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1859 }) 1860 1861 # --- stop orders sector data: 1862 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1863 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1864 1865 for item in view["raw"]["stopOrders"]: 1866 self.figi = item["figi"] 1867 1868 if item["figi"] not in uniqueStopOrdersFIGIs: 1869 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1870 1871 uniqueStopOrdersFIGIs.append(item["figi"]) 1872 uniqueStopOrders[item["figi"]] = instrument 1873 1874 else: 1875 instrument = uniqueStopOrders[item["figi"]] 1876 1877 if instrument: 1878 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1879 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1880 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1881 1882 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1883 if "expirationTime" in item.keys(): 1884 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1885 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1886 1887 else: 1888 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1889 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1890 1891 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1892 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1893 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1894 1895 else: 1896 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1897 1898 # requested price when stop-order executed: 1899 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1900 1901 # price for limit-order, set up when stop-order executed: 1902 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1903 1904 # necessary changes in percent to reach target from current price: 1905 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1906 1907 view["stat"]["stopOrders"].append({ 1908 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1909 "figi": item["figi"], # FIGI identification 1910 "ticker": instrument["ticker"], # ticker name by FIGI 1911 "lotsRequested": item["lotsRequested"], # requested lots value 1912 "currentPrice": lastPrice, # current instrument's price for defined action 1913 "targetPrice": target, # requested price for stop-order execution in base currency 1914 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1915 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1916 "percentChanges": changes, # changes in percent to target from current price 1917 "currency": item["currency"], # instrument's currency name 1918 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1919 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1920 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1921 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1922 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1923 }) 1924 1925 # --- calculating data for analytics section: 1926 # portfolio distribution by assets: 1927 view["analytics"]["distrByAssets"] = { 1928 "Ruble": { 1929 "uniques": 1, 1930 "cost": view["stat"]["availableRUB"], 1931 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1932 }, 1933 "Currencies": { 1934 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1935 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1936 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1937 }, 1938 "Shares": { 1939 "uniques": len(view["stat"]["Shares"]), 1940 "cost": view["stat"]["sharesCostRUB"], 1941 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1942 }, 1943 "Bonds": { 1944 "uniques": len(view["stat"]["Bonds"]), 1945 "cost": view["stat"]["bondsCostRUB"], 1946 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1947 }, 1948 "Etfs": { 1949 "uniques": len(view["stat"]["Etfs"]), 1950 "cost": view["stat"]["etfsCostRUB"], 1951 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1952 }, 1953 "Futures": { 1954 "uniques": len(view["stat"]["Futures"]), 1955 "cost": view["stat"]["futuresCostRUB"], 1956 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1957 }, 1958 } 1959 1960 # portfolio distribution by companies: 1961 view["analytics"]["distrByCompanies"]["All money cash"] = { 1962 "ticker": "", 1963 "cost": view["stat"]["allCurrenciesCostRUB"], 1964 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1965 } 1966 view["analytics"]["distrByCompanies"].update(byComp) 1967 1968 # portfolio distribution by sectors: 1969 view["analytics"]["distrBySectors"]["All money cash"] = { 1970 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 1971 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 1972 } 1973 view["analytics"]["distrBySectors"].update(bySect) 1974 1975 # portfolio distribution by currencies: 1976 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 1977 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 1978 1979 if self.moreDebug: 1980 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 1981 1982 view["analytics"]["distrByCurrencies"].update(byCurr) 1983 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1984 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1985 1986 # portfolio distribution by countries: 1987 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 1988 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 1989 1990 if self.moreDebug: 1991 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 1992 1993 view["analytics"]["distrByCountries"].update(byCountry) 1994 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1995 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1996 1997 # --- Prepare text statistics overview in human-readable: 1998 if show: 1999 # Whatever the value `details`, header not changes: 2000 info = [ 2001 "# Client's portfolio\n\n", 2002 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2003 "* **Account ID:** [{}]\n".format(self.accountId), 2004 ] 2005 2006 if details in ["full", "positions", "digest"]: 2007 info.extend([ 2008 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2009 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2010 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2011 view["stat"]["totalChangesRUB"], 2012 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2013 view["stat"]["totalChangesPercentRUB"], 2014 ), 2015 ]) 2016 2017 if details in ["full", "positions"]: 2018 info.extend([ 2019 "## Open positions\n\n", 2020 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2021 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2022 "| Ruble | {:>31} | | | | | |\n".format( 2023 "{:.2f} ({:.2f}) rub".format( 2024 view["stat"]["availableRUB"], 2025 view["stat"]["blockedRUB"], 2026 ) 2027 ) 2028 ]) 2029 2030 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2031 return [ 2032 "| | | | | | | |\n", 2033 "| {:<27} | | | | | {:>19} | |\n".format( 2034 noTradeStr if noTradeStr else typeStr, 2035 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2036 ), 2037 ] 2038 2039 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2040 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2041 "{} [{}]".format(data["ticker"], data["figi"]), 2042 "{:.2f} ({:.2f}) {}".format( 2043 data["volume"], 2044 data["blocked"], 2045 data["currency"], 2046 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2047 data["volume"], 2048 data["blocked"], 2049 ), 2050 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2051 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2052 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2053 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2054 "{}{:.2f} {} ({}{:.2f}%)".format( 2055 "+" if data["profit"] > 0 else "", 2056 data["profit"], data["baseCurrencyName"], 2057 "+" if data["percentProfit"] > 0 else "", 2058 data["percentProfit"], 2059 ), 2060 ) 2061 2062 # --- Show currencies section: 2063 if view["stat"]["Currencies"]: 2064 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2065 for item in view["stat"]["Currencies"]: 2066 info.append(_InfoStr(item, showCurrencyName=True)) 2067 2068 else: 2069 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2070 2071 # --- Show shares section: 2072 if view["stat"]["Shares"]: 2073 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2074 2075 for item in view["stat"]["Shares"]: 2076 info.append(_InfoStr(item)) 2077 2078 else: 2079 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2080 2081 # --- Show bonds section: 2082 if view["stat"]["Bonds"]: 2083 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2084 2085 for item in view["stat"]["Bonds"]: 2086 info.append(_InfoStr(item)) 2087 2088 else: 2089 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2090 2091 # --- Show etfs section: 2092 if view["stat"]["Etfs"]: 2093 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2094 2095 for item in view["stat"]["Etfs"]: 2096 info.append(_InfoStr(item)) 2097 2098 else: 2099 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2100 2101 # --- Show futures section: 2102 if view["stat"]["Futures"]: 2103 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2104 2105 for item in view["stat"]["Futures"]: 2106 info.append(_InfoStr(item)) 2107 2108 else: 2109 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2110 2111 if details in ["full", "orders"]: 2112 # --- Show pending limit orders section: 2113 if view["stat"]["orders"]: 2114 info.extend([ 2115 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2116 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2117 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2118 ]) 2119 2120 for item in view["stat"]["orders"]: 2121 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2122 "{} [{}]".format(item["ticker"], item["figi"]), 2123 item["orderID"], 2124 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2125 "{} {} ({}{:.2f}%)".format( 2126 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2127 item["baseCurrencyName"], 2128 "+" if item["percentChanges"] > 0 else "", 2129 float(item["percentChanges"]), 2130 ), 2131 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2132 item["action"], 2133 item["type"], 2134 item["date"], 2135 )) 2136 2137 else: 2138 info.append("\n## Total pending limit-orders: 0\n") 2139 2140 # --- Show stop orders section: 2141 if view["stat"]["stopOrders"]: 2142 info.extend([ 2143 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2144 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2145 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2146 ]) 2147 2148 for item in view["stat"]["stopOrders"]: 2149 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2150 "{} [{}]".format(item["ticker"], item["figi"]), 2151 item["orderID"], 2152 item["lotsRequested"], 2153 "{} {} ({}{:.2f}%)".format( 2154 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2155 item["baseCurrencyName"], 2156 "+" if item["percentChanges"] > 0 else "", 2157 float(item["percentChanges"]), 2158 ), 2159 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2160 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2161 item["action"], 2162 item["type"], 2163 item["expType"], 2164 item["createDate"], 2165 item["expDate"], 2166 )) 2167 2168 else: 2169 info.append("\n## Total stop-orders: 0\n") 2170 2171 if details in ["full", "analytics"]: 2172 # -- Show analytics section: 2173 if view["stat"]["portfolioCostRUB"] > 0: 2174 info.extend([ 2175 "\n# Analytics\n" 2176 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2177 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2178 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2179 view["stat"]["totalChangesRUB"], 2180 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2181 view["stat"]["totalChangesPercentRUB"], 2182 ), 2183 "\n## Portfolio distribution by assets\n" 2184 "\n| Type | Uniques | Percent | Current cost |\n", 2185 "|------------------------------------|---------|---------|--------------------|\n", 2186 ]) 2187 2188 for key in view["analytics"]["distrByAssets"].keys(): 2189 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2190 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2191 key, 2192 view["analytics"]["distrByAssets"][key]["uniques"], 2193 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2194 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2195 )) 2196 2197 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2198 2199 info.extend([ 2200 "\n## Portfolio distribution by companies\n" 2201 "\n| Company | Percent | Current cost |\n", 2202 aSepLine, 2203 ]) 2204 2205 for company in view["analytics"]["distrByCompanies"].keys(): 2206 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2207 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2208 "{}{}".format( 2209 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2210 company, 2211 ), 2212 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2213 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2214 )) 2215 2216 info.extend([ 2217 "\n## Portfolio distribution by sectors\n" 2218 "\n| Sector | Percent | Current cost |\n", 2219 aSepLine, 2220 ]) 2221 2222 for sector in view["analytics"]["distrBySectors"].keys(): 2223 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2224 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2225 sector, 2226 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2227 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2228 )) 2229 2230 info.extend([ 2231 "\n## Portfolio distribution by currencies\n" 2232 "\n| Instruments currencies | Percent | Current cost |\n", 2233 aSepLine, 2234 ]) 2235 2236 for curr in view["analytics"]["distrByCurrencies"].keys(): 2237 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2238 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2239 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2240 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2241 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2242 )) 2243 2244 info.extend([ 2245 "\n## Portfolio distribution by countries\n" 2246 "\n| Assets by country | Percent | Current cost |\n", 2247 aSepLine, 2248 ]) 2249 2250 for country in view["analytics"]["distrByCountries"].keys(): 2251 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2252 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2253 country, 2254 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2255 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2256 )) 2257 2258 if details in ["full", "calendar"]: 2259 # -- Show bonds payment calendar section: 2260 if view["stat"]["Bonds"]: 2261 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2262 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2263 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2264 2265 else: 2266 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2267 2268 infoText = "".join(info) 2269 2270 uLogger.info(infoText) 2271 2272 if details == "full" and self.overviewFile: 2273 filename = self.overviewFile 2274 2275 elif details == "digest" and self.overviewDigestFile: 2276 filename = self.overviewDigestFile 2277 2278 elif details == "positions" and self.overviewPositionsFile: 2279 filename = self.overviewPositionsFile 2280 2281 elif details == "orders" and self.overviewOrdersFile: 2282 filename = self.overviewOrdersFile 2283 2284 elif details == "analytics" and self.overviewAnalyticsFile: 2285 filename = self.overviewAnalyticsFile 2286 2287 elif details == "calendar" and self.overviewBondsCalendarFile: 2288 filename = self.overviewBondsCalendarFile 2289 2290 else: 2291 filename = "" 2292 2293 if filename: 2294 with open(filename, "w", encoding="UTF-8") as fH: 2295 fH.write(infoText) 2296 2297 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2298 2299 return view 2300 2301 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2302 """ 2303 Returns history operations between two given dates for current `accountId`. 2304 If `reportFile` string is not empty then also save human-readable report. 2305 Shows some statistical data of closed positions. 2306 2307 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2308 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2309 :param show: if `True` then also prints all records to the console. 2310 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2311 :return: original list of dictionaries with history of deals records from API ("operations" key): 2312 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2313 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2314 """ 2315 if self.accountId is None or not self.accountId: 2316 uLogger.error("Variable `accountId` must be defined for using this method!") 2317 raise Exception("Account ID required") 2318 2319 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2320 2321 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2322 2323 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2324 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2325 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2326 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2327 customStat = {} # custom statistics in additional to responseJSON 2328 2329 # --- output report in human-readable format: 2330 if show or self.reportFile: 2331 splitLine1 = "| | | | | |\n" # Summary section 2332 splitLine2 = "| | | | | | | | |\n" # Operations section 2333 nextDay = "" 2334 2335 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2336 2337 if len(ops) > 0: 2338 customStat = { 2339 "opsCount": 0, # total operations count 2340 "buyCount": 0, # buy operations 2341 "sellCount": 0, # sell operations 2342 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2343 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2344 "payIn": {"rub": 0.}, # Deposit brokerage account 2345 "payOut": {"rub": 0.}, # Withdrawals 2346 "divs": {"rub": 0.}, # Dividends income 2347 "coupons": {"rub": 0.}, # Coupon's income 2348 "brokerCom": {"rub": 0.}, # Service commissions 2349 "serviceCom": {"rub": 0.}, # Service commissions 2350 "marginCom": {"rub": 0.}, # Margin commissions 2351 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2352 } 2353 2354 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2355 for item in ops: 2356 if item["state"] == "OPERATION_STATE_EXECUTED": 2357 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2358 2359 # count buy operations: 2360 if "_BUY" in item["operationType"]: 2361 customStat["buyCount"] += 1 2362 2363 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2364 customStat["buyTotal"][item["payment"]["currency"]] += payment 2365 2366 else: 2367 customStat["buyTotal"][item["payment"]["currency"]] = payment 2368 2369 # count sell operations: 2370 elif "_SELL" in item["operationType"]: 2371 customStat["sellCount"] += 1 2372 2373 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2374 customStat["sellTotal"][item["payment"]["currency"]] += payment 2375 2376 else: 2377 customStat["sellTotal"][item["payment"]["currency"]] = payment 2378 2379 # count incoming operations: 2380 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2381 if item["payment"]["currency"] in customStat["payIn"].keys(): 2382 customStat["payIn"][item["payment"]["currency"]] += payment 2383 2384 else: 2385 customStat["payIn"][item["payment"]["currency"]] = payment 2386 2387 # count withdrawals operations: 2388 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2389 if item["payment"]["currency"] in customStat["payOut"].keys(): 2390 customStat["payOut"][item["payment"]["currency"]] += payment 2391 2392 else: 2393 customStat["payOut"][item["payment"]["currency"]] = payment 2394 2395 # count dividends income: 2396 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2397 if item["payment"]["currency"] in customStat["divs"].keys(): 2398 customStat["divs"][item["payment"]["currency"]] += payment 2399 2400 else: 2401 customStat["divs"][item["payment"]["currency"]] = payment 2402 2403 # count coupon's income: 2404 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2405 if item["payment"]["currency"] in customStat["coupons"].keys(): 2406 customStat["coupons"][item["payment"]["currency"]] += payment 2407 2408 else: 2409 customStat["coupons"][item["payment"]["currency"]] = payment 2410 2411 # count broker commissions: 2412 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2413 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2414 customStat["brokerCom"][item["payment"]["currency"]] += payment 2415 2416 else: 2417 customStat["brokerCom"][item["payment"]["currency"]] = payment 2418 2419 # count service commissions: 2420 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2421 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2422 customStat["serviceCom"][item["payment"]["currency"]] += payment 2423 2424 else: 2425 customStat["serviceCom"][item["payment"]["currency"]] = payment 2426 2427 # count margin commissions: 2428 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2429 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2430 customStat["marginCom"][item["payment"]["currency"]] += payment 2431 2432 else: 2433 customStat["marginCom"][item["payment"]["currency"]] = payment 2434 2435 # count withholding taxes: 2436 elif "_TAX" in item["operationType"]: 2437 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2438 customStat["allTaxes"][item["payment"]["currency"]] += payment 2439 2440 else: 2441 customStat["allTaxes"][item["payment"]["currency"]] = payment 2442 2443 else: 2444 continue 2445 2446 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2447 2448 # --- view "Actions" lines: 2449 info.extend([ 2450 "| Report sections | | | | |\n", 2451 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2452 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2453 "| | Buy: {:<22} | {:<28} | | |\n".format( 2454 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2455 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2456 ), 2457 "| | Sell: {:<21} | {:<28} | | |\n".format( 2458 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2459 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2460 ), 2461 ]) 2462 2463 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2464 for key in opsKeys: 2465 if key == "rub": 2466 continue 2467 2468 info.extend([ 2469 "| | | {:<28} | | |\n".format( 2470 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2471 ), 2472 "| | | {:<28} | | |\n".format( 2473 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2474 ), 2475 ]) 2476 2477 info.append(splitLine1) 2478 2479 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2480 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2481 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2482 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2483 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2484 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2485 ) 2486 2487 # --- view "Payments" lines: 2488 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2489 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2490 2491 for key in paymentsKeys: 2492 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2493 2494 info.append(splitLine1) 2495 2496 # --- view "Commissions and taxes" lines: 2497 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2498 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2499 2500 for key in comKeys: 2501 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2502 2503 info.append(splitLine1) 2504 2505 info.extend([ 2506 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2507 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2508 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2509 ]) 2510 2511 else: 2512 info.append("Broker returned no operations during this period\n") 2513 2514 # --- view "Operations" section: 2515 for item in ops: 2516 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2517 continue 2518 2519 else: 2520 self.figi = item["figi"] if item["figi"] else "" 2521 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2522 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2523 2524 # group of deals during one day: 2525 if nextDay and item["date"].split("T")[0] != nextDay: 2526 info.append(splitLine2) 2527 nextDay = "" 2528 2529 else: 2530 nextDay = item["date"].split("T")[0] # saving current day for splitting 2531 2532 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2533 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2534 self.figi if self.figi else "—", 2535 instrument["ticker"] if instrument else "—", 2536 instrument["type"] if instrument else "—", 2537 item["quantity"] if int(item["quantity"]) > 0 else "—", 2538 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2539 TKS_OPERATION_STATES[item["state"]], 2540 TKS_OPERATION_TYPES[item["operationType"]], 2541 )) 2542 2543 infoText = "".join(info) 2544 2545 if show: 2546 if self.moreDebug: 2547 uLogger.debug("Records about history of a client's operations successfully received") 2548 2549 uLogger.info(infoText) 2550 2551 if self.reportFile: 2552 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2553 fH.write(infoText) 2554 2555 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2556 2557 return ops, customStat 2558 2559 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2560 """ 2561 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2562 2563 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2564 Warning! Broker server used ISO UTC time by default. 2565 2566 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2567 Also, `historyFile` used to update history with `onlyMissing` parameter. 2568 2569 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2570 2571 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2572 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2573 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2574 `"hour"`, `"day"`. Default: `"hour"`. 2575 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2576 False by default. Warning! History appends only from last candle to current time 2577 with always update last candle! 2578 :param csvSep: separator if csv-file is used, `,` by default. 2579 :param show: if `True` then also prints Pandas DataFrame to the console. 2580 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2581 `["date", "time", "open", "high", "low", "close", "volume"]`. 2582 """ 2583 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2584 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2585 history = None # empty pandas object for history 2586 2587 if interval not in TKS_CANDLE_INTERVALS.keys(): 2588 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2589 raise Exception("Incorrect value") 2590 2591 if not (self.ticker or self.figi): 2592 uLogger.error("Ticker or FIGI must be defined!") 2593 raise Exception("Ticker or FIGI required") 2594 2595 if self.ticker and not self.figi: 2596 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2597 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2598 2599 if self.figi and not self.ticker: 2600 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2601 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2602 2603 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2604 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2605 if interval.lower() != "day": 2606 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2607 2608 delta = dtEnd - dtStart # current UTC time minus last time in file 2609 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2610 2611 # calculate history length in candles: 2612 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2613 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2614 length += 1 # to avoid fraction time 2615 2616 # calculate data blocks count: 2617 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2618 2619 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2620 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2621 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2622 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2623 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2624 2625 tempOld = None # pandas object for old history, if --only-missing key present 2626 lastTime = None # datetime object of last old candle in file 2627 2628 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2629 uLogger.debug("--only-missing key present, add only last missing candles...") 2630 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2631 2632 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2633 2634 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2635 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2636 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2637 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2638 2639 # get last datetime object from last string in file or minus 1 delta if file is empty: 2640 if len(tempOld) > 0: 2641 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2642 2643 else: 2644 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2645 2646 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2647 2648 responseJSONs = [] # raw history blocks of data 2649 2650 blockEnd = dtEnd 2651 for item in range(blocks): 2652 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2653 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2654 2655 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2656 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2657 )) 2658 2659 if blockStart == blockEnd: 2660 uLogger.debug("Skipped this zero-length block...") 2661 2662 else: 2663 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2664 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2665 self.body = str({ 2666 "figi": self.figi, 2667 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2668 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2669 "interval": TKS_CANDLE_INTERVALS[interval][0] 2670 }) 2671 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2672 2673 if "code" in responseJSON.keys(): 2674 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2675 2676 else: 2677 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2678 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2679 2680 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2681 2682 blockEnd = blockStart 2683 2684 printCount = len(responseJSONs) # candles to show in console 2685 if responseJSONs: 2686 tempHistory = pd.DataFrame( 2687 data={ 2688 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2689 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2690 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2691 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2692 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2693 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2694 "volume": [int(item["volume"]) for item in responseJSONs], 2695 }, 2696 index=range(len(responseJSONs)), 2697 columns=["date", "time", "open", "high", "low", "close", "volume"], 2698 ) 2699 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2700 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2701 2702 # append only newest candles to old history if --only-missing key present: 2703 if onlyMissing and tempOld is not None and lastTime is not None: 2704 index = 0 # find start index in tempHistory data: 2705 2706 for i, item in tempHistory.iterrows(): 2707 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2708 2709 if curTime == lastTime: 2710 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2711 index = i 2712 printCount = index + 1 2713 break 2714 2715 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2716 2717 else: 2718 history = tempHistory # if no `--only-missing` key then load full data from server 2719 2720 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2721 2722 if history is not None and not history.empty: 2723 if show: 2724 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2725 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2726 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2727 )) 2728 2729 else: 2730 uLogger.warning("Received an empty candles history!") 2731 2732 if self.historyFile is not None: 2733 if history is not None and not history.empty: 2734 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2735 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2736 2737 else: 2738 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2739 2740 else: 2741 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2742 2743 return history 2744 2745 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2746 """ 2747 Load candles history from csv-file and return Pandas DataFrame object. 2748 2749 See also: `History()` and `ShowHistoryChart()` methods. 2750 2751 :param filePath: path to csv-file to open. 2752 """ 2753 loadedHistory = None # init candles data object 2754 2755 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2756 2757 if os.path.exists(filePath): 2758 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2759 2760 tfStr = self.priceModel.FormattedDelta( 2761 self.priceModel.timeframe, 2762 "{days} days {hours}h {minutes}m {seconds}s", 2763 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2764 self.priceModel.timeframe, 2765 "{hours}h {minutes}m {seconds}s", 2766 ) 2767 2768 if loadedHistory is not None and not loadedHistory.empty: 2769 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2770 len(loadedHistory), 2771 tfStr, 2772 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2773 ) 2774 2775 else: 2776 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2777 2778 else: 2779 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2780 2781 return loadedHistory 2782 2783 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2784 """ 2785 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2786 2787 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2788 Default: `index.html` (both for interact and non-interact candlesticks chart). 2789 2790 See also: `History()` and `LoadHistory()` methods. 2791 2792 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2793 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2794 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2795 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2796 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2797 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2798 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2799 """ 2800 if isinstance(candles, str): 2801 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2802 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2803 2804 elif isinstance(candles, pd.DataFrame): 2805 self.priceModel.prices = candles # set candles chain from variable 2806 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2807 2808 if "datetime" not in candles.columns: 2809 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2810 2811 else: 2812 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2813 raise Exception("Incorrect value") 2814 2815 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2816 2817 if interact: 2818 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2819 2820 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2821 2822 else: 2823 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2824 2825 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2826 2827 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2828 2829 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2830 """ 2831 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2832 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2833 2834 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2835 2836 :param operation: string "Buy" or "Sell". 2837 :param lots: volume, integer count of lots >= 1. 2838 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2839 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2840 :param expDate: string "Undefined" by default or local date in future, 2841 it is a string with format `%Y-%m-%d %H:%M:%S`. 2842 :return: JSON with response from broker server. 2843 """ 2844 if self.accountId is None or not self.accountId: 2845 uLogger.error("Variable `accountId` must be defined for using this method!") 2846 raise Exception("Account ID required") 2847 2848 if operation is None or not operation or operation not in ("Buy", "Sell"): 2849 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2850 raise Exception("Incorrect value") 2851 2852 if lots is None or lots < 1: 2853 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2854 lots = 1 2855 2856 if tp is None or tp < 0: 2857 tp = 0 2858 2859 if sl is None or sl < 0: 2860 sl = 0 2861 2862 if expDate is None or not expDate: 2863 expDate = "Undefined" 2864 2865 if not (self.ticker or self.figi): 2866 uLogger.error("Ticker or FIGI must be defined!") 2867 raise Exception("Ticker or FIGI required") 2868 2869 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2870 self.ticker = instrument["ticker"] 2871 self.figi = instrument["figi"] 2872 2873 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2874 2875 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2876 self.body = str({ 2877 "figi": self.figi, 2878 "quantity": str(lots), 2879 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2880 "accountId": str(self.accountId), 2881 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2882 }) 2883 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2884 2885 if "orderId" in response.keys(): 2886 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2887 operation, response["orderId"], 2888 self.ticker, self.figi, lots, 2889 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2890 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2891 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2892 )) 2893 2894 if tp > 0: 2895 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2896 2897 if sl > 0: 2898 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2899 2900 else: 2901 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 2902 2903 return response 2904 2905 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2906 """ 2907 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2908 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2909 2910 See also: `Order()` and `Trade()` docstrings. 2911 2912 :param lots: volume, integer count of lots >= 1. 2913 :param tp: float > 0, take profit price of stop-order. 2914 :param sl: float > 0, stop loss price of stop-order. 2915 :param expDate: it's a local date in future. 2916 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2917 :return: JSON with response from broker server. 2918 """ 2919 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2920 2921 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2922 """ 2923 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2924 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2925 2926 See also: `Order()` and `Trade()` docstrings. 2927 2928 :param lots: volume, integer count of lots >= 1. 2929 :param tp: float > 0, take profit price of stop-order. 2930 :param sl: float > 0, stop loss price of stop-order. 2931 :param expDate: it's a local date in the future. 2932 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2933 :return: JSON with response from broker server. 2934 """ 2935 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2936 2937 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 2938 """ 2939 Close position of given instruments. 2940 2941 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 2942 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2943 This avoids unnecessary downloading data from the server. 2944 """ 2945 if instruments is None or not instruments: 2946 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 2947 raise Exception("Ticker or FIGI required") 2948 2949 if isinstance(instruments, str): 2950 instruments = [instruments] 2951 2952 uniqueInstruments = self.GetUniqueFIGIs(instruments) 2953 if uniqueInstruments: 2954 if portfolio is None or not portfolio: 2955 portfolio = self.Overview(show=False) 2956 2957 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2958 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 2959 2960 for self.figi in uniqueInstruments: 2961 if self.figi not in allOpened: 2962 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 2963 continue 2964 2965 # search open trade info about instrument by ticker: 2966 instrument = {} 2967 for iType in TKS_INSTRUMENTS: 2968 if instrument: 2969 break 2970 2971 for item in portfolio["stat"][iType]: 2972 if item["figi"] == self.figi: 2973 instrument = item 2974 break 2975 2976 if instrument: 2977 self.ticker = instrument["ticker"] 2978 self.figi = instrument["figi"] 2979 2980 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 2981 self.ticker, 2982 self.figi, 2983 int(instrument["volume"]), 2984 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 2985 )) 2986 2987 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 2988 2989 if tradeLots > 0: 2990 if instrument["blocked"] > 0: 2991 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 2992 instrument["blocked"], 2993 self.ticker, 2994 tradeLots, 2995 )) 2996 2997 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 2998 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 2999 3000 else: 3001 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3002 3003 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3004 """ 3005 Close all positions of given instruments with defined type. 3006 3007 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3008 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3009 This avoids unnecessary downloading data from the server. 3010 """ 3011 if iType not in TKS_INSTRUMENTS: 3012 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3013 3014 else: 3015 if portfolio is None or not portfolio: 3016 portfolio = self.Overview(show=False) 3017 3018 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3019 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3020 3021 if tickers and portfolio: 3022 self.CloseTrades(tickers, portfolio) 3023 3024 else: 3025 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3026 3027 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3028 """ 3029 Universal method to create market or limit orders with all available parameters for current `accountId`. 3030 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3031 3032 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3033 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3034 3035 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3036 then broker immediately open market order as you can do simple --buy or --sell operations! 3037 3038 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3039 When current price will go up or down to target price value then broker opens a limit order. 3040 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3041 3042 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3043 3044 :param operation: string "Buy" or "Sell". 3045 :param orderType: string "Limit" or "Stop". 3046 :param lots: volume, integer count of lots >= 1. 3047 :param targetPrice: target price > 0. This is open trade price for limit order. 3048 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3049 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3050 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3051 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3052 Stop loss order always executed by market price. 3053 :param expDate: string "Undefined" by default or local date in future. 3054 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3055 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3056 A limit order has no expiration date, it lasts until the end of the trading day. 3057 :return: JSON with response from broker server. 3058 """ 3059 if self.accountId is None or not self.accountId: 3060 uLogger.error("Variable `accountId` must be defined for using this method!") 3061 raise Exception("Account ID required") 3062 3063 if operation is None or not operation or operation not in ("Buy", "Sell"): 3064 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3065 raise Exception("Incorrect value") 3066 3067 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3068 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3069 raise Exception("Incorrect value") 3070 3071 if lots is None or lots < 1: 3072 uLogger.error("You must define trade volume > 0: integer count of lots!") 3073 raise Exception("Incorrect value") 3074 3075 if targetPrice is None or targetPrice <= 0: 3076 uLogger.error("Target price for limit-order must be greater than 0!") 3077 raise Exception("Incorrect value") 3078 3079 if limitPrice is None or limitPrice <= 0: 3080 limitPrice = targetPrice 3081 3082 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3083 stopType = "Limit" 3084 3085 if expDate is None or not expDate: 3086 expDate = "Undefined" 3087 3088 if not (self.ticker or self.figi): 3089 uLogger.error("Tocker or FIGI must be defined!") 3090 raise Exception("Ticker or FIGI required") 3091 3092 response = {} 3093 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3094 self.ticker = instrument["ticker"] 3095 self.figi = instrument["figi"] 3096 3097 if orderType == "Limit": 3098 uLogger.debug( 3099 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3100 self.ticker, self.figi, 3101 operation, lots, targetPrice, instrument["currency"], 3102 )) 3103 3104 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3105 self.body = str({ 3106 "figi": self.figi, 3107 "quantity": str(lots), 3108 "price": FloatToNano(targetPrice), 3109 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3110 "accountId": str(self.accountId), 3111 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3112 }) 3113 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3114 3115 if "orderId" in response.keys(): 3116 uLogger.info( 3117 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3118 response["orderId"], 3119 self.ticker, self.figi, 3120 operation, lots, targetPrice, instrument["currency"], 3121 )) 3122 3123 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3124 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3125 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3126 targetPrice, instrument["currency"], 3127 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3128 )) 3129 3130 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3131 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3132 targetPrice, instrument["currency"], 3133 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3134 )) 3135 3136 else: 3137 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3138 3139 if orderType == "Stop": 3140 uLogger.debug( 3141 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3142 self.ticker, self.figi, 3143 operation, lots, 3144 targetPrice, instrument["currency"], 3145 limitPrice, instrument["currency"], 3146 stopType, expDate, 3147 )) 3148 3149 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3150 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3151 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3152 3153 body = { 3154 "figi": self.figi, 3155 "quantity": str(lots), 3156 "price": FloatToNano(limitPrice), 3157 "stopPrice": FloatToNano(targetPrice), 3158 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3159 "accountId": str(self.accountId), 3160 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3161 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3162 } 3163 3164 if expDateUTC: 3165 body["expireDate"] = expDateUTC 3166 3167 self.body = str(body) 3168 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3169 3170 if "stopOrderId" in response.keys(): 3171 uLogger.info( 3172 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3173 response["stopOrderId"], 3174 self.ticker, self.figi, 3175 operation, lots, 3176 targetPrice, instrument["currency"], 3177 limitPrice, instrument["currency"], 3178 TKS_STOP_ORDER_TYPES[stopOrderType], 3179 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3180 )) 3181 3182 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3183 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3184 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3185 targetPrice, instrument["currency"], 3186 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3187 )) 3188 3189 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3190 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3191 targetPrice, instrument["currency"], 3192 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3193 )) 3194 3195 else: 3196 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3197 3198 return response 3199 3200 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3201 """ 3202 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3203 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3204 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3205 See also: `Order()` docstring. 3206 3207 :param lots: volume, integer count of lots >= 1. 3208 :param targetPrice: target price > 0. This is open trade price for limit order. 3209 :return: JSON with response from broker server. 3210 """ 3211 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3212 3213 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3214 """ 3215 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3216 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3217 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3218 target price value then broker opens a limit order. See also: `Order()` docstring. 3219 3220 :param lots: volume, integer count of lots >= 1. 3221 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3222 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3223 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3224 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3225 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3226 :param expDate: string "Undefined" by default or local date in future. 3227 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3228 This date is converting to UTC format for server. 3229 :return: JSON with response from broker server. 3230 """ 3231 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3232 3233 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3234 """ 3235 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3236 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3237 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3238 See also: `Order()` docstring. 3239 3240 :param lots: volume, integer count of lots >= 1. 3241 :param targetPrice: target price > 0. This is open trade price for limit order. 3242 :return: JSON with response from broker server. 3243 """ 3244 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3245 3246 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3247 """ 3248 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3249 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3250 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3251 target price value then broker opens a limit order. See also: `Order()` docstring. 3252 3253 :param lots: volume, integer count of lots >= 1. 3254 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3255 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3256 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3257 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3258 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3259 :param expDate: string "Undefined" by default or local date in future. 3260 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3261 This date is converting to UTC format for server. 3262 :return: JSON with response from broker server. 3263 """ 3264 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3265 3266 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3267 """ 3268 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3269 3270 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3271 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3272 This avoids unnecessary downloading data from the server. 3273 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3274 """ 3275 if self.accountId is None or not self.accountId: 3276 uLogger.error("Variable `accountId` must be defined for using this method!") 3277 raise Exception("Account ID required") 3278 3279 if orderIDs: 3280 if allOrdersIDs is None: 3281 rawOrders = self.RequestPendingOrders() 3282 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3283 3284 if allStopOrdersIDs is None: 3285 rawStopOrders = self.RequestStopOrders() 3286 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3287 3288 for orderID in orderIDs: 3289 idInPendingOrders = orderID in allOrdersIDs 3290 idInStopOrders = orderID in allStopOrdersIDs 3291 3292 if not (idInPendingOrders or idInStopOrders): 3293 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3294 continue 3295 3296 else: 3297 if idInPendingOrders: 3298 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3299 3300 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3301 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3302 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3303 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3304 3305 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3306 if self.moreDebug: 3307 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3308 3309 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3310 3311 else: 3312 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3313 3314 elif idInStopOrders: 3315 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3316 3317 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3318 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3319 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3320 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3321 3322 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3323 if self.moreDebug: 3324 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3325 3326 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3327 3328 else: 3329 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3330 3331 else: 3332 continue 3333 3334 def CloseAllOrders(self) -> None: 3335 """ 3336 Gets a list of open pending and stop orders and cancel it all. 3337 """ 3338 rawOrders = self.RequestPendingOrders() 3339 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3340 lenOrders = len(allOrdersIDs) 3341 3342 rawStopOrders = self.RequestStopOrders() 3343 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3344 lenSOrders = len(allStopOrdersIDs) 3345 3346 if lenOrders > 0 or lenSOrders > 0: 3347 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3348 3349 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3350 3351 else: 3352 uLogger.info("Orders not found, nothing to cancel.") 3353 3354 def CloseAll(self, *args) -> None: 3355 """ 3356 Close all available (not blocked) opened trades and orders. 3357 3358 Also, you can select one or more keywords case-insensitive: 3359 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3360 3361 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3362 """ 3363 overview = self.Overview(show=False) # get all open trades info 3364 3365 if len(args) == 0: 3366 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3367 self.CloseAllOrders() # close all pending and stop orders 3368 3369 for iType in TKS_INSTRUMENTS: 3370 if iType != "Currencies": 3371 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3372 3373 else: 3374 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3375 lowerArgs = [x.lower() for x in args] 3376 3377 if "orders" in lowerArgs: 3378 self.CloseAllOrders() # close all pending and stop orders 3379 3380 for iType in TKS_INSTRUMENTS: 3381 if iType.lower() in lowerArgs and iType != "Currencies": 3382 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3383 3384 def CloseAllByTicker(self, instrument: str) -> None: 3385 """ 3386 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3387 3388 This method searches opened trade and orders of instrument throw all portfolio and then use 3389 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3390 3391 :param instrument: string with ticker. 3392 """ 3393 if instrument is None or not instrument: 3394 uLogger.error("Ticker name must be defined for using this method!") 3395 raise Exception("Ticker required") 3396 3397 overview = self.Overview(show=False) # get user portfolio with all open trades info 3398 3399 self.ticker = instrument # try to set instrument as ticker 3400 self.figi = "" 3401 3402 if self.IsInPortfolio(portfolio=overview): 3403 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3404 self.CloseTrades(instruments=[instrument], portfolio=overview) 3405 3406 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3407 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3408 3409 if limitAll and self.IsInLimitOrders(portfolio=overview): 3410 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3411 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3412 3413 if stopAll and self.IsInStopOrders(portfolio=overview): 3414 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3415 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3416 3417 def CloseAllByFIGI(self, instrument: str) -> None: 3418 """ 3419 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3420 3421 This method searches opened trade and orders of instrument throw all portfolio and then use 3422 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3423 3424 :param instrument: string with FIGI id. 3425 """ 3426 if instrument is None or not instrument: 3427 uLogger.error("FIGI id must be defined for using this method!") 3428 raise Exception("FIGI required") 3429 3430 overview = self.Overview(show=False) # get user portfolio with all open trades info 3431 3432 self.ticker = "" 3433 self.figi = instrument # try to set instrument as FIGI id 3434 3435 if self.IsInPortfolio(portfolio=overview): 3436 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3437 self.CloseTrades(instruments=[instrument], portfolio=overview) 3438 3439 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3440 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3441 3442 if limitAll and self.IsInLimitOrders(portfolio=overview): 3443 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3444 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3445 3446 if stopAll and self.IsInStopOrders(portfolio=overview): 3447 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3448 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3449 3450 @staticmethod 3451 def ParseOrderParameters(operation, **inputParameters): 3452 """ 3453 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3454 3455 :param operation: string "Buy" or "Sell". 3456 :param inputParameters: this is dict of strings that looks like this 3457 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3458 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3459 "prices" key: one or more prices to open limit-orders 3460 Counts of values in lots and prices lists must be equals! 3461 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3462 """ 3463 # TODO: update order grid work with api v2 3464 pass 3465 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3466 # 3467 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3468 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3469 # raise Exception("Incorrect value") 3470 # 3471 # if "l" in inputParameters.keys(): 3472 # inputParameters["lots"] = inputParameters.pop("l") 3473 # 3474 # if "p" in inputParameters.keys(): 3475 # inputParameters["prices"] = inputParameters.pop("p") 3476 # 3477 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3478 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3479 # raise Exception("Incorrect value") 3480 # 3481 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3482 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3483 # 3484 # if len(lots) != len(prices): 3485 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3486 # raise Exception("Incorrect value") 3487 # 3488 # uLogger.debug("Extracted parameters for orders:") 3489 # uLogger.debug("lots = {}".format(lots)) 3490 # uLogger.debug("prices = {}".format(prices)) 3491 # 3492 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3493 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3494 # uLogger.debug("Order parameters: {}".format(result)) 3495 # 3496 # return result 3497 3498 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3499 """ 3500 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3501 3502 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3503 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3504 """ 3505 result = False 3506 msg = "Instrument not defined!" 3507 3508 if portfolio is None or not portfolio: 3509 portfolio = self.Overview(show=False) 3510 3511 if self.ticker: 3512 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self.ticker)) 3513 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3514 3515 for iType in TKS_INSTRUMENTS: 3516 for instrument in portfolio["stat"][iType]: 3517 if instrument["ticker"] == self.ticker: 3518 result = True 3519 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3520 break 3521 3522 elif self.figi: 3523 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self.figi)) 3524 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3525 3526 for iType in TKS_INSTRUMENTS: 3527 for instrument in portfolio["stat"][iType]: 3528 if instrument["figi"] == self.figi: 3529 result = True 3530 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3531 break 3532 3533 else: 3534 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3535 3536 uLogger.debug(msg) 3537 3538 return result 3539 3540 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3541 """ 3542 Returns instrument from the user's portfolio if it presents there. 3543 Instrument must be defined by `ticker` (highly priority) or `figi`. 3544 3545 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3546 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3547 """ 3548 result = None 3549 msg = "Instrument not defined!" 3550 3551 if portfolio is None or not portfolio: 3552 portfolio = self.Overview(show=False) 3553 3554 if self.ticker: 3555 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker)) 3556 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3557 3558 for iType in TKS_INSTRUMENTS: 3559 for instrument in portfolio["stat"][iType]: 3560 if instrument["ticker"] == self.ticker: 3561 result = instrument 3562 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3563 break 3564 3565 elif self.figi: 3566 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3567 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3568 3569 for iType in TKS_INSTRUMENTS: 3570 for instrument in portfolio["stat"][iType]: 3571 if instrument["figi"] == self.figi: 3572 result = instrument 3573 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3574 break 3575 3576 else: 3577 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3578 3579 uLogger.debug(msg) 3580 3581 return result 3582 3583 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3584 """ 3585 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3586 3587 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3588 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3589 """ 3590 result = False 3591 msg = "Instrument not defined!" 3592 3593 if portfolio is None or not portfolio: 3594 portfolio = self.Overview(show=False) 3595 3596 if self.ticker: 3597 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self.ticker)) 3598 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self.ticker) 3599 3600 for instrument in portfolio["stat"]["orders"]: 3601 if instrument["ticker"] == self.ticker: 3602 result = True 3603 msg = "Instrument with ticker [{}] is present in limit orders list".format(self.ticker) 3604 break 3605 3606 elif self.figi: 3607 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self.figi)) 3608 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self.figi) 3609 3610 for instrument in portfolio["stat"]["orders"]: 3611 if instrument["figi"] == self.figi: 3612 result = True 3613 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self.figi) 3614 break 3615 3616 else: 3617 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3618 3619 uLogger.debug(msg) 3620 3621 return result 3622 3623 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3624 """ 3625 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3626 Instrument must be defined by `ticker` (highly priority) or `figi`. 3627 3628 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3629 :return: list with `orderID`s of limit orders. 3630 """ 3631 result = [] 3632 msg = "Instrument not defined!" 3633 3634 if portfolio is None or not portfolio: 3635 portfolio = self.Overview(show=False) 3636 3637 if self.ticker: 3638 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self.ticker)) 3639 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self.ticker) 3640 3641 for instrument in portfolio["stat"]["orders"]: 3642 if instrument["ticker"] == self.ticker: 3643 result.append(instrument["orderID"]) 3644 3645 if result: 3646 msg = "Instrument with ticker [{}] is present in limit orders list".format(self.ticker) 3647 3648 elif self.figi: 3649 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self.figi)) 3650 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self.figi) 3651 3652 for instrument in portfolio["stat"]["orders"]: 3653 if instrument["figi"] == self.figi: 3654 result.append(instrument["orderID"]) 3655 3656 if result: 3657 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self.figi) 3658 3659 else: 3660 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3661 3662 uLogger.debug(msg) 3663 3664 return result 3665 3666 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3667 """ 3668 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3669 3670 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3671 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3672 """ 3673 result = False 3674 msg = "Instrument not defined!" 3675 3676 if portfolio is None or not portfolio: 3677 portfolio = self.Overview(show=False) 3678 3679 if self.ticker: 3680 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self.ticker)) 3681 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self.ticker) 3682 3683 for instrument in portfolio["stat"]["stopOrders"]: 3684 if instrument["ticker"] == self.ticker: 3685 result = True 3686 msg = "Instrument with ticker [{}] is present in stop orders list".format(self.ticker) 3687 break 3688 3689 elif self.figi: 3690 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self.figi)) 3691 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self.figi) 3692 3693 for instrument in portfolio["stat"]["stopOrders"]: 3694 if instrument["figi"] == self.figi: 3695 result = True 3696 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self.figi) 3697 break 3698 3699 else: 3700 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3701 3702 uLogger.debug(msg) 3703 3704 return result 3705 3706 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3707 """ 3708 Returns list with all `orderID`s of opened stop orders for the instrument. 3709 Instrument must be defined by `ticker` (highly priority) or `figi`. 3710 3711 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3712 :return: list with `orderID`s of stop orders. 3713 """ 3714 result = [] 3715 msg = "Instrument not defined!" 3716 3717 if portfolio is None or not portfolio: 3718 portfolio = self.Overview(show=False) 3719 3720 if self.ticker: 3721 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self.ticker)) 3722 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self.ticker) 3723 3724 for instrument in portfolio["stat"]["stopOrders"]: 3725 if instrument["ticker"] == self.ticker: 3726 result.append(instrument["orderID"]) 3727 3728 if result: 3729 msg = "Instrument with ticker [{}] is present in stop orders list".format(self.ticker) 3730 3731 elif self.figi: 3732 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self.figi)) 3733 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self.figi) 3734 3735 for instrument in portfolio["stat"]["stopOrders"]: 3736 if instrument["figi"] == self.figi: 3737 result.append(instrument["orderID"]) 3738 3739 if result: 3740 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self.figi) 3741 3742 else: 3743 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3744 3745 uLogger.debug(msg) 3746 3747 return result 3748 3749 def RequestLimits(self) -> dict: 3750 """ 3751 Method for obtaining the available funds for withdrawal for current `accountId`. 3752 3753 See also: 3754 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3755 - `OverviewLimits()` method 3756 3757 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3758 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3759 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3760 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3761 """ 3762 if self.accountId is None or not self.accountId: 3763 uLogger.error("Variable `accountId` must be defined for using this method!") 3764 raise Exception("Account ID required") 3765 3766 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3767 3768 self.body = str({"accountId": self.accountId}) 3769 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3770 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3771 3772 if self.moreDebug: 3773 uLogger.debug("Records about available funds for withdrawal successfully received") 3774 3775 return rawLimits 3776 3777 def OverviewLimits(self, show: bool = False) -> dict: 3778 """ 3779 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3780 3781 See also: `RequestLimits()`. 3782 3783 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3784 :return: dict with raw parsed data from server and some calculated statistics about it. 3785 """ 3786 if self.accountId is None or not self.accountId: 3787 uLogger.error("Variable `accountId` must be defined for using this method!") 3788 raise Exception("Account ID required") 3789 3790 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3791 3792 view = { 3793 "rawLimits": rawLimits, 3794 "limits": { # parsed data for every currency: 3795 "money": { # this is an array of portfolio currency positions 3796 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3797 }, 3798 "blocked": { # this is an array of blocked currency 3799 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3800 }, 3801 "blockedGuarantee": { # this is locked money under collateral for futures 3802 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3803 }, 3804 }, 3805 } 3806 3807 # --- Prepare text table with limits in human-readable format: 3808 if show: 3809 info = [ 3810 "# Withdrawal limits\n\n", 3811 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3812 "* **Account ID:** [{}]\n".format(self.accountId), 3813 ] 3814 3815 if view["limits"]["money"]: 3816 info.extend([ 3817 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3818 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3819 ]) 3820 3821 else: 3822 info.append("\nNo withdrawal limits\n") 3823 3824 for curr in view["limits"]["money"].keys(): 3825 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3826 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3827 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3828 3829 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3830 "[{}]".format(curr), 3831 "{:.2f}".format(view["limits"]["money"][curr]), 3832 "{:.2f}".format(availableMoney), 3833 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3834 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3835 ) 3836 3837 if curr == "rub": 3838 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3839 3840 else: 3841 info.append(infoStr) 3842 3843 infoText = "".join(info) 3844 3845 uLogger.info(infoText) 3846 3847 if self.withdrawalLimitsFile: 3848 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3849 fH.write(infoText) 3850 3851 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3852 3853 return view 3854 3855 def RequestAccounts(self) -> dict: 3856 """ 3857 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3858 3859 See also: 3860 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3861 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3862 - `OverviewUserInfo()` method 3863 3864 :return: dict with raw data from server that contains accounts info. Example of dict: 3865 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3866 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3867 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3868 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3869 """ 3870 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3871 3872 self.body = str({}) 3873 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3874 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3875 3876 if self.moreDebug: 3877 uLogger.debug("Records about available accounts successfully received") 3878 3879 return rawAccounts 3880 3881 def RequestUserInfo(self) -> dict: 3882 """ 3883 Method for requesting common user's information. 3884 3885 See also: 3886 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3887 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3888 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3889 - `OverviewUserInfo()` method 3890 3891 :return: dict with raw data from server that contains user's information. Example of dict: 3892 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3893 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3894 """ 3895 uLogger.debug("Requesting common user's information. Wait, please...") 3896 3897 self.body = str({}) 3898 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3899 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3900 3901 if self.moreDebug: 3902 uLogger.debug("Records about current user successfully received") 3903 3904 return rawUserInfo 3905 3906 def RequestMarginStatus(self, accountId: str = None) -> dict: 3907 """ 3908 Method for requesting margin calculation for defined account ID. 3909 3910 See also: 3911 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3912 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3913 - `OverviewUserInfo()` method 3914 3915 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3916 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3917 Example of responses: 3918 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3919 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3920 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3921 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3922 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3923 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3924 """ 3925 if accountId is None or not accountId: 3926 if self.accountId is None or not self.accountId: 3927 uLogger.error("Variable `accountId` must be defined for using this method!") 3928 raise Exception("Account ID required") 3929 3930 else: 3931 accountId = self.accountId # use `self.accountId` (main ID) by default 3932 3933 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3934 3935 self.body = str({"accountId": accountId}) 3936 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3937 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3938 3939 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3940 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3941 rawMargin = {} 3942 3943 else: 3944 if self.moreDebug: 3945 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3946 3947 return rawMargin 3948 3949 def RequestTariffLimits(self) -> dict: 3950 """ 3951 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3952 3953 See also: 3954 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3955 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3956 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3957 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3958 - `OverviewUserInfo()` method 3959 3960 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3961 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3962 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3963 """ 3964 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3965 3966 self.body = str({}) 3967 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3968 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3969 3970 if self.moreDebug: 3971 uLogger.debug("Records with limits of current tariff successfully received") 3972 3973 return rawTariffLimits 3974 3975 def RequestBondCoupons(self, iJSON: dict) -> dict: 3976 """ 3977 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3978 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3979 All dates are in UTC timezone. 3980 3981 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3982 Documentation: 3983 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3984 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3985 3986 See also: `ExtendBondsData()`. 3987 3988 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3989 If raw iJSON is not data of bond then server returns an error [400] with message: 3990 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3991 :return: dictionary with bond payment calendar. Response example 3992 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3993 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3994 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3995 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3996 """ 3997 if iJSON["figi"] is None or not iJSON["figi"]: 3998 uLogger.error("FIGI must be defined for using this method!") 3999 raise Exception("FIGI required") 4000 4001 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4002 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4003 4004 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4005 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4006 self.figi, 4007 startDate, 4008 endDate, 4009 )) 4010 4011 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4012 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4013 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4014 4015 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4016 uLogger.warning("Instrument type is not bond!") 4017 4018 else: 4019 if self.moreDebug: 4020 uLogger.debug("Records about bond payment calendar successfully received") 4021 4022 return calendar 4023 4024 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4025 """ 4026 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4027 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4028 coupon yields, current yields and some statistics etc. 4029 4030 WARNING! This is too long operation if a lot of bonds requested from broker server. 4031 4032 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4033 4034 :param instruments: list of strings with tickers or FIGIs. 4035 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4036 for further used by data scientists or stock analytics. 4037 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4038 In XLSX-file and Pandas DataFrame fields mean: 4039 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4040 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4041 """ 4042 if instruments is None or not instruments: 4043 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4044 raise Exception("Ticker or FIGI required") 4045 4046 if isinstance(instruments, str): 4047 instruments = [instruments] 4048 4049 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4050 4051 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4052 4053 iCount = len(uniqueInstruments) 4054 tooLong = iCount >= 20 4055 if tooLong: 4056 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4057 4058 bonds = None 4059 for i, self.figi in enumerate(uniqueInstruments): 4060 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4061 4062 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4063 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4064 rawBond = self.SearchByFIGI(requestPrice=True) 4065 4066 # Widen raw data with UTC current time (iData["actualDateTime"]): 4067 actualDate = datetime.now(tzutc()) 4068 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4069 4070 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4071 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4072 4073 # Replace some values with human-readable: 4074 iData["nominalCurrency"] = iData["nominal"]["currency"] 4075 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4076 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4077 iData["aciCurrency"] = iData["aciValue"]["currency"] 4078 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4079 iData["issueSize"] = int(iData["issueSize"]) 4080 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4081 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4082 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4083 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4084 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4085 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4086 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4087 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4088 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4089 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4090 4091 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4092 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4093 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4094 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4095 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4096 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4097 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4098 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4099 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4100 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4101 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4102 4103 # Widen raw data with calendar data from `rawCalendar` values: 4104 calendarData = [] 4105 if "events" in iData["rawCalendar"].keys(): 4106 for item in iData["rawCalendar"]["events"]: 4107 calendarData.append({ 4108 "couponDate": item["couponDate"], 4109 "couponNumber": int(item["couponNumber"]), 4110 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4111 "payCurrency": item["payOneBond"]["currency"], 4112 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4113 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4114 "couponStartDate": item["couponStartDate"], 4115 "couponEndDate": item["couponEndDate"], 4116 "couponPeriod": item["couponPeriod"], 4117 }) 4118 4119 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4120 if "maturityDate" not in iData.keys(): 4121 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4122 4123 # Widen raw data with Coupon Rate. 4124 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4125 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4126 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4127 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4128 4129 # Widen raw data with Yield to Maturity (YTM) on current date. 4130 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4131 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4132 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4133 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4134 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4135 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4136 4137 iData["calendar"] = calendarData # adds calendar at the end 4138 4139 # Remove not used data: 4140 iData.pop("uid") 4141 iData.pop("positionUid") 4142 iData.pop("currentPrice") 4143 iData.pop("rawCalendar") 4144 4145 colNames = list(iData.keys()) 4146 if bonds is None: 4147 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4148 4149 else: 4150 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4151 4152 else: 4153 uLogger.warning("Instrument is not a bond!") 4154 4155 processed = round(100 * (i + 1) / iCount, 1) 4156 if tooLong and processed % 5 == 0: 4157 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4158 4159 else: 4160 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4161 4162 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4163 4164 # Saving bonds from Pandas DataFrame to XLSX sheet: 4165 if xlsx and self.bondsXLSXFile: 4166 with pd.ExcelWriter( 4167 path=self.bondsXLSXFile, 4168 date_format=TKS_DATE_FORMAT, 4169 datetime_format=TKS_DATE_TIME_FORMAT, 4170 mode="w", 4171 ) as writer: 4172 bonds.to_excel( 4173 writer, 4174 sheet_name="Extended bonds data", 4175 index=True, 4176 encoding="UTF-8", 4177 freeze_panes=(1, 1), 4178 ) # saving as XLSX-file with freeze first row and column as headers 4179 4180 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4181 4182 return bonds 4183 4184 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4185 """ 4186 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4187 4188 WARNING! This is too long operation if a lot of bonds requested from broker server. 4189 4190 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4191 4192 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4193 extended information about bonds: main info, current prices, bond payment calendar, 4194 coupon yields, current yields and some statistics etc. 4195 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4196 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4197 for further used by data scientists or stock analytics. 4198 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4199 """ 4200 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4201 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4202 4203 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4204 4205 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4206 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4207 calendar = None 4208 for bond in extBonds.iterrows(): 4209 for item in bond[1]["calendar"]: 4210 cData = { 4211 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4212 "couponDate": item["couponDate"], 4213 "figi": bond[1]["figi"], 4214 "ticker": bond[1]["ticker"], 4215 "name": bond[1]["name"], 4216 "couponNumber": item["couponNumber"], 4217 "payOneBond": item["payOneBond"], 4218 "payCurrency": item["payCurrency"], 4219 "couponType": item["couponType"], 4220 "couponPeriod": item["couponPeriod"], 4221 "fixDate": item["fixDate"], 4222 "couponStartDate": item["couponStartDate"], 4223 "couponEndDate": item["couponEndDate"], 4224 } 4225 4226 if calendar is None: 4227 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4228 4229 else: 4230 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4231 4232 if calendar is not None: 4233 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4234 4235 # Saving calendar from Pandas DataFrame to XLSX sheet: 4236 if xlsx: 4237 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4238 4239 with pd.ExcelWriter( 4240 path=xlsxCalendarFile, 4241 date_format=TKS_DATE_FORMAT, 4242 datetime_format=TKS_DATE_TIME_FORMAT, 4243 mode="w", 4244 ) as writer: 4245 humanReadable = calendar.copy(deep=True) 4246 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4247 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4248 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4249 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4250 humanReadable.columns = colNames # human-readable column names 4251 4252 humanReadable.to_excel( 4253 writer, 4254 sheet_name="Bond payments calendar", 4255 index=False, 4256 encoding="UTF-8", 4257 freeze_panes=(1, 2), 4258 ) # saving as XLSX-file with freeze first row and column as headers 4259 4260 del humanReadable # release df in memory 4261 4262 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4263 4264 return calendar 4265 4266 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4267 """ 4268 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4269 Also, creates Markdown file with calendar data, `calendar.md` by default. 4270 4271 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4272 4273 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4274 extended information about bonds: main info, current prices, bond payment calendar, 4275 coupon yields, current yields and some statistics etc. 4276 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4277 :param show: if `True` then also printing bonds payment calendar to the console, 4278 otherwise save to file `calendarFile` only. `False` by default. 4279 :return: multilines text in Markdown format with bonds payment calendar as a table. 4280 """ 4281 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4282 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4283 4284 infoText = "# Bond payments calendar\n\n" 4285 4286 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4287 4288 if not (calendar is None or calendar.empty): 4289 splitLine = "| | | | | | | | | |\n" 4290 4291 info = [ 4292 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4293 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4294 ] 4295 4296 newMonth = False 4297 notOneBond = calendar["figi"].nunique() > 1 4298 for i, bond in enumerate(calendar.iterrows()): 4299 if newMonth and notOneBond: 4300 info.append(splitLine) 4301 4302 info.append( 4303 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4304 " √" if bond[1]["paid"] else " —", 4305 bond[1]["couponDate"].split("T")[0], 4306 bond[1]["figi"], 4307 bond[1]["ticker"], 4308 bond[1]["couponNumber"], 4309 "{} {}".format( 4310 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4311 bond[1]["payCurrency"], 4312 ), 4313 bond[1]["couponType"], 4314 bond[1]["couponPeriod"], 4315 bond[1]["fixDate"].split("T")[0], 4316 ) 4317 ) 4318 4319 if i < len(calendar.values) - 1: 4320 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4321 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4322 newMonth = False if curDate.month == nextDate.month else True 4323 4324 else: 4325 newMonth = False 4326 4327 infoText += "".join(info) 4328 4329 if show: 4330 uLogger.info("{}".format(infoText)) 4331 4332 if self.calendarFile is not None: 4333 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4334 fH.write(infoText) 4335 4336 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4337 4338 else: 4339 infoText += "No data\n" 4340 4341 return infoText 4342 4343 def OverviewAccounts(self, show: bool = False) -> dict: 4344 """ 4345 Method for parsing and show simple table with all available user accounts. 4346 4347 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4348 4349 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4350 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4351 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4352 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4353 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4354 "closed": "—", "access": "Full access" }, ...}}` 4355 """ 4356 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4357 4358 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4359 accounts = { 4360 item["id"]: { 4361 "type": TKS_ACCOUNT_TYPES[item["type"]], 4362 "name": item["name"], 4363 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4364 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4365 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4366 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4367 } for item in rawAccounts["accounts"] 4368 } 4369 4370 # Raw and parsed data with some fields replaced in "stat" section: 4371 view = { 4372 "rawAccounts": rawAccounts, 4373 "stat": accounts, 4374 } 4375 4376 # --- Prepare simple text table with only accounts data in human-readable format: 4377 if show: 4378 info = [ 4379 "# User accounts\n\n", 4380 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4381 "| Account ID | Type | Status | Name |\n", 4382 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4383 ] 4384 4385 for account in view["stat"].keys(): 4386 info.extend([ 4387 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4388 account, 4389 view["stat"][account]["type"], 4390 view["stat"][account]["status"], 4391 view["stat"][account]["name"], 4392 ) 4393 ]) 4394 4395 infoText = "".join(info) 4396 4397 uLogger.info(infoText) 4398 4399 if self.userAccountsFile: 4400 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4401 fH.write(infoText) 4402 4403 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4404 4405 return view 4406 4407 def OverviewUserInfo(self, show: bool = False) -> dict: 4408 """ 4409 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4410 4411 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4412 4413 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4414 :return: dict with raw parsed data from server and some calculated statistics about it. 4415 """ 4416 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4417 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4418 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4419 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4420 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4421 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4422 4423 # This is dict with parsed common user data: 4424 userInfo = { 4425 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4426 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4427 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4428 "tariff": rawUserInfo["tariff"], 4429 } 4430 4431 # This is an array of dict with parsed margin statuses for every account IDs: 4432 margins = {} 4433 for accountId in accounts.keys(): 4434 if rawMargins[accountId]: 4435 margins[accountId] = { 4436 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4437 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4438 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4439 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4440 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4441 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4442 } 4443 4444 else: 4445 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4446 4447 unary = {} # unary-connection limits 4448 for item in rawTariffLimits["unaryLimits"]: 4449 if item["limitPerMinute"] in unary.keys(): 4450 unary[item["limitPerMinute"]].extend(item["methods"]) 4451 4452 else: 4453 unary[item["limitPerMinute"]] = item["methods"] 4454 4455 stream = {} # stream-connection limits 4456 for item in rawTariffLimits["streamLimits"]: 4457 if item["limit"] in stream.keys(): 4458 stream[item["limit"]].extend(item["streams"]) 4459 4460 else: 4461 stream[item["limit"]] = item["streams"] 4462 4463 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4464 limits = { 4465 "unary": unary, 4466 "stream": stream, 4467 } 4468 4469 # Raw and parsed data as an output result: 4470 view = { 4471 "rawUserInfo": rawUserInfo, 4472 "rawAccounts": rawAccounts, 4473 "rawMargins": rawMargins, 4474 "rawTariffLimits": rawTariffLimits, 4475 "stat": { 4476 "userInfo": userInfo, 4477 "accounts": accounts, 4478 "margins": margins, 4479 "limits": limits, 4480 }, 4481 } 4482 4483 # --- Prepare text table with user information in human-readable format: 4484 if show: 4485 info = [ 4486 "# Full user information\n\n", 4487 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4488 "## Common information\n\n", 4489 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4490 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4491 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4492 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4493 "\n## User accounts\n\n", 4494 ] 4495 4496 for account in view["stat"]["accounts"].keys(): 4497 info.extend([ 4498 "### ID: [{}]\n\n".format(account), 4499 "| Parameters | Values |\n", 4500 "|----------------------|--------------------------------------------------------------|\n", 4501 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4502 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4503 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4504 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4505 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4506 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4507 ]) 4508 4509 if margins[account]: 4510 info.extend([ 4511 "| Margin status: | Enabled |\n", 4512 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4513 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4514 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4515 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4516 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4517 ]) 4518 4519 else: 4520 info.append("| Margin status: | Disabled |\n\n") 4521 4522 info.extend([ 4523 "\n## Current user tariff limits\n", 4524 "\nSee also:\n", 4525 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4526 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4527 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4528 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4529 "\n### Unary limits\n", 4530 ]) 4531 4532 if unary: 4533 for key, values in sorted(unary.items()): 4534 info.append("\n* Max requests per minute: {}\n".format(key)) 4535 4536 for value in values: 4537 info.append(" - {}\n".format(value)) 4538 4539 else: 4540 info.append("\nNot available\n") 4541 4542 info.append("\n### Stream limits\n") 4543 4544 if stream: 4545 for key, values in sorted(stream.items()): 4546 info.append("\n* Max stream connections: {}\n".format(key)) 4547 4548 for value in values: 4549 info.append(" - {}\n".format(value)) 4550 4551 else: 4552 info.append("\nNot available\n") 4553 4554 infoText = "".join(info) 4555 4556 uLogger.info(infoText) 4557 4558 if self.userInfoFile: 4559 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4560 fH.write(infoText) 4561 4562 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4563 4564 return view 4565 4566 4567class Args: 4568 """ 4569 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4570 """ 4571 def __init__(self, **kwargs): 4572 self.__dict__.update(kwargs) 4573 4574 def __getattr__(self, item): 4575 return None 4576 4577 4578def ParseArgs(): 4579 """This function get and parse command line keys.""" 4580 parser = ArgumentParser() # command-line string parser 4581 4582 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4583 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4584 4585 # --- options: 4586 4587 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4588 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4589 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4590 4591 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4592 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4593 4594 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4595 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4596 4597 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4598 4599 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4600 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4601 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4602 4603 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4604 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4605 4606 # --- commands: 4607 4608 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4609 4610 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4611 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4612 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4613 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4614 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4615 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4616 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4617 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4618 4619 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4620 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4621 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4622 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4623 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4624 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4625 4626 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4627 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4628 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4629 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4630 4631 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4632 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4633 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4634 4635 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4636 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4637 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4638 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4639 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4640 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4641 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4642 4643 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4644 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4645 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4646 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4647 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4648 4649 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4650 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4651 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4652 4653 cmdArgs = parser.parse_args() 4654 return cmdArgs 4655 4656 4657def Main(**kwargs): 4658 """ 4659 Main function for work with TKSBrokerAPI in the console. 4660 4661 See examples: 4662 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4663 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4664 """ 4665 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4666 4667 if args.debug_level: 4668 uLogger.level = 10 # always debug level by default 4669 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4670 4671 exitCode = 0 4672 start = datetime.now(tzutc()) 4673 uLogger.debug("=-" * 50) 4674 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4675 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4676 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4677 )) 4678 4679 # trying to calculate full current version: 4680 buildVersion = __version__ 4681 try: 4682 v = version("tksbrokerapi") 4683 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4684 4685 except Exception: 4686 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4687 4688 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4689 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4690 4691 try: 4692 if args.version: 4693 print("TKSBrokerAPI {}".format(buildVersion)) 4694 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4695 4696 else: 4697 # Init class for trading with Tinkoff Broker: 4698 trader = TinkoffBrokerServer( 4699 token=args.token, 4700 accountId=args.account_id, 4701 useCache=not args.no_cache, 4702 ) 4703 4704 # --- set some options: 4705 4706 if args.more: 4707 trader.moreDebug = True 4708 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4709 4710 if args.ticker: 4711 ticker = args.ticker.upper() # Tickers may be upper case only 4712 4713 if ticker in trader.aliasesKeys: 4714 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4715 4716 else: 4717 trader.ticker = ticker 4718 4719 if args.figi: 4720 trader.figi = args.figi.upper() # FIGIs may be upper case only 4721 4722 if args.depth is not None: 4723 trader.depth = args.depth 4724 4725 # --- do one command: 4726 4727 if args.list: 4728 if args.output is not None: 4729 trader.instrumentsFile = args.output 4730 4731 trader.ShowInstrumentsInfo(show=True) 4732 4733 elif args.list_xlsx: 4734 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4735 4736 elif args.bonds_xlsx is not None: 4737 if args.output is not None: 4738 trader.bondsXLSXFile = args.output 4739 4740 if len(args.bonds_xlsx) == 0: 4741 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4742 4743 else: 4744 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4745 4746 elif args.search: 4747 if args.output is not None: 4748 trader.searchResultsFile = args.output 4749 4750 trader.SearchInstruments(pattern=args.search[0], show=True) 4751 4752 elif args.info: 4753 if not (args.ticker or args.figi): 4754 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4755 raise Exception("Ticker or FIGI required") 4756 4757 if args.output is not None: 4758 trader.infoFile = args.output 4759 4760 if args.ticker: 4761 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4762 4763 else: 4764 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4765 4766 elif args.calendar is not None: 4767 if args.output is not None: 4768 trader.calendarFile = args.output 4769 4770 if len(args.calendar) == 0: 4771 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4772 4773 else: 4774 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4775 4776 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4777 4778 elif args.price: 4779 if not (args.ticker or args.figi): 4780 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4781 raise Exception("Ticker or FIGI required") 4782 4783 trader.GetCurrentPrices(show=True) 4784 4785 elif args.prices is not None: 4786 if args.output is not None: 4787 trader.pricesFile = args.output 4788 4789 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4790 4791 elif args.overview: 4792 if args.output is not None: 4793 trader.overviewFile = args.output 4794 4795 trader.Overview(show=True, details="full") 4796 4797 elif args.overview_digest: 4798 if args.output is not None: 4799 trader.overviewDigestFile = args.output 4800 4801 trader.Overview(show=True, details="digest") 4802 4803 elif args.overview_positions: 4804 if args.output is not None: 4805 trader.overviewPositionsFile = args.output 4806 4807 trader.Overview(show=True, details="positions") 4808 4809 elif args.overview_orders: 4810 if args.output is not None: 4811 trader.overviewOrdersFile = args.output 4812 4813 trader.Overview(show=True, details="orders") 4814 4815 elif args.overview_analytics: 4816 if args.output is not None: 4817 trader.overviewAnalyticsFile = args.output 4818 4819 trader.Overview(show=True, details="analytics") 4820 4821 elif args.overview_calendar: 4822 if args.output is not None: 4823 trader.overviewAnalyticsFile = args.output 4824 4825 trader.Overview(show=True, details="calendar") 4826 4827 elif args.deals is not None: 4828 if args.output is not None: 4829 trader.reportFile = args.output 4830 4831 if 0 <= len(args.deals) < 3: 4832 trader.Deals( 4833 start=args.deals[0] if len(args.deals) >= 1 else None, 4834 end=args.deals[1] if len(args.deals) == 2 else None, 4835 show=True, # Always show deals report in console 4836 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4837 ) 4838 4839 else: 4840 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4841 raise Exception("Incorrect value") 4842 4843 elif args.history is not None: 4844 if args.output is not None: 4845 trader.historyFile = args.output 4846 4847 if 0 <= len(args.history) < 3: 4848 dataReceived = trader.History( 4849 start=args.history[0] if len(args.history) >= 1 else None, 4850 end=args.history[1] if len(args.history) == 2 else None, 4851 interval="hour" if args.interval is None or not args.interval else args.interval, 4852 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4853 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4854 show=True, # shows all downloaded candles in console 4855 ) 4856 4857 if args.render_chart is not None and dataReceived is not None: 4858 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4859 4860 trader.ShowHistoryChart( 4861 candles=dataReceived, 4862 interact=iChart, 4863 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4864 ) 4865 4866 else: 4867 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4868 raise Exception("Incorrect value") 4869 4870 elif args.load_history is not None: 4871 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4872 4873 if args.render_chart is not None and histData is not None: 4874 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4875 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4876 4877 trader.ShowHistoryChart( 4878 candles=histData, 4879 interact=iChart, 4880 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4881 ) 4882 4883 elif args.trade is not None: 4884 if 1 <= len(args.trade) <= 5: 4885 trader.Trade( 4886 operation=args.trade[0], 4887 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4888 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4889 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4890 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4891 ) 4892 4893 else: 4894 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4895 4896 elif args.buy is not None: 4897 if 0 <= len(args.buy) <= 4: 4898 trader.Buy( 4899 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4900 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4901 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4902 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4903 ) 4904 4905 else: 4906 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4907 4908 elif args.sell is not None: 4909 if 0 <= len(args.sell) <= 4: 4910 trader.Sell( 4911 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4912 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4913 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4914 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4915 ) 4916 4917 else: 4918 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4919 4920 elif args.order: 4921 if 4 <= len(args.order) <= 7: 4922 trader.Order( 4923 operation=args.order[0], 4924 orderType=args.order[1], 4925 lots=int(args.order[2]), 4926 targetPrice=float(args.order[3]), 4927 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4928 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4929 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4930 ) 4931 4932 else: 4933 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4934 4935 elif args.buy_limit: 4936 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4937 4938 elif args.sell_limit: 4939 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4940 4941 elif args.buy_stop: 4942 if 2 <= len(args.buy_stop) <= 7: 4943 trader.BuyStop( 4944 lots=int(args.buy_stop[0]), 4945 targetPrice=float(args.buy_stop[1]), 4946 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4947 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4948 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4949 ) 4950 4951 else: 4952 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4953 4954 elif args.sell_stop: 4955 if 2 <= len(args.sell_stop) <= 7: 4956 trader.SellStop( 4957 lots=int(args.sell_stop[0]), 4958 targetPrice=float(args.sell_stop[1]), 4959 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4960 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4961 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4962 ) 4963 4964 else: 4965 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4966 4967 # elif args.buy_order_grid is not None: 4968 # # update order grid work with api v2 4969 # if len(args.buy_order_grid) == 2: 4970 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4971 # 4972 # for order in orderParams: 4973 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4974 # 4975 # else: 4976 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4977 # 4978 # elif args.sell_order_grid is not None: 4979 # # update order grid work with api v2 4980 # if len(args.sell_order_grid) >= 2: 4981 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4982 # 4983 # for order in orderParams: 4984 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4985 # 4986 # else: 4987 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4988 4989 elif args.close_order is not None: 4990 trader.CloseOrders(args.close_order) # close only one order 4991 4992 elif args.close_orders is not None: 4993 trader.CloseOrders(args.close_orders) # close list of orders 4994 4995 elif args.close_trade: 4996 if not (args.ticker or args.figi): 4997 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4998 raise Exception("Ticker or FIGI required") 4999 5000 if args.ticker: 5001 trader.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 5002 5003 else: 5004 trader.CloseTrades([args.figi]) # close only one trade by FIGI 5005 5006 elif args.close_trades is not None: 5007 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5008 5009 elif args.close_all is not None: 5010 if args.ticker: 5011 trader.CloseAllByTicker(instrument=args.ticker) 5012 5013 elif args.figi: 5014 trader.CloseAllByFIGI(instrument=args.figi) 5015 5016 else: 5017 trader.CloseAll(*args.close_all) 5018 5019 elif args.limits: 5020 if args.output is not None: 5021 trader.withdrawalLimitsFile = args.output 5022 5023 trader.OverviewLimits(show=True) 5024 5025 elif args.user_info: 5026 if args.output is not None: 5027 trader.userInfoFile = args.output 5028 5029 trader.OverviewUserInfo(show=True) 5030 5031 elif args.account: 5032 if args.output is not None: 5033 trader.userAccountsFile = args.output 5034 5035 trader.OverviewAccounts(show=True) 5036 5037 else: 5038 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5039 raise Exception("There is no command to execute") 5040 5041 except Exception: 5042 trace = tb.format_exc() 5043 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5044 if e in trace: 5045 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5046 break 5047 5048 uLogger.debug(trace) 5049 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5050 exitCode = 255 # an error occurred, must be open a ticket for this issue 5051 5052 finally: 5053 finish = datetime.now(tzutc()) 5054 5055 if exitCode == 0: 5056 if args.more: 5057 uLogger.debug("All operations were finished success (summary code is 0).") 5058 5059 else: 5060 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5061 os.path.abspath(uLog.defaultLogFile), exitCode, 5062 )) 5063 5064 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5065 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5066 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5067 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5068 )) 5069 uLogger.debug("=-" * 50) 5070 5071 if not kwargs: 5072 sys.exit(exitCode) 5073 5074 else: 5075 return exitCode 5076 5077 5078if __name__ == "__main__": 5079 Main()
76class TinkoffBrokerServer: 77 """ 78 This class implements methods to work with Tinkoff broker server. 79 80 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 81 82 About `token`: https://tinkoff.github.io/investAPI/token/ 83 """ 84 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 85 """ 86 Main class init. 87 88 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 89 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 90 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 91 :param useCache: use default cache file with raw data to use instead of `iList`. 92 True by default. Cache is auto-update if new day has come. 93 If you don't want to use cache and always updates raw data then set `useCache=False`. 94 :param defaultCache: path to default cache file. `dump.json` by default. 95 """ 96 if token is None or not token: 97 try: 98 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 99 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 100 101 except KeyError: 102 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 103 raise Exception("Token required") 104 105 else: 106 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 107 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 108 109 if accountId is None or not accountId: 110 try: 111 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 112 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 113 114 except KeyError: 115 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 116 117 else: 118 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 119 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 120 121 self.version = __version__ # duplicate here used TKSBrokerAPI main version 122 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 123 124 Latest version: https://pypi.org/project/tksbrokerapi/ 125 """ 126 127 self.aliases = TKS_TICKER_ALIASES 128 """Some aliases instead official tickers. 129 130 See also: `TKSEnums.TKS_TICKER_ALIASES` 131 """ 132 133 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 134 135 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 136 137 self.ticker = "" 138 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 139 140 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 141 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 142 143 See also: `SearchByTicker()`, `SearchInstruments()`. 144 """ 145 146 self.figi = "" 147 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 148 149 See also: `SearchByFIGI()`, `SearchInstruments()`. 150 """ 151 152 self.depth = 1 153 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 154 155 See also: `GetCurrentPrices()`. 156 """ 157 158 self.server = r"https://invest-public-api.tinkoff.ru/rest" 159 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 160 161 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 162 """ 163 164 uLogger.debug("Broker API server: {}".format(self.server)) 165 166 self.timeout = 15 167 """Server operations timeout in seconds. Default: `15`. 168 169 See also: `SendAPIRequest()`. 170 """ 171 172 self.headers = { 173 "Content-Type": "application/json", 174 "accept": "application/json", 175 "Authorization": "Bearer {}".format(self.token), 176 "x-app-name": "Tim55667757.TKSBrokerAPI", 177 } 178 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 179 180 See also: `SendAPIRequest()`. 181 """ 182 183 self.body = None 184 """Request body which send to broker server. Default: `None`. 185 186 See also: `SendAPIRequest()`. 187 """ 188 189 self.moreDebug = False 190 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 191 192 self.historyFile = None 193 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 194 195 See also: `History()`. 196 """ 197 198 self.htmlHistoryFile = "index.html" 199 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 200 201 See also: `ShowHistoryChart()`. 202 """ 203 204 self.instrumentsFile = "instruments.md" 205 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 206 207 See also: `ShowInstrumentsInfo()`. 208 """ 209 210 self.searchResultsFile = "search-results.md" 211 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 212 213 See also: `SearchInstruments()`. 214 """ 215 216 self.pricesFile = "prices.md" 217 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 218 219 See also: `GetListOfPrices()`. 220 """ 221 222 self.infoFile = "info.md" 223 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 224 225 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 226 """ 227 228 self.bondsXLSXFile = "ext-bonds.xlsx" 229 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 230 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 231 232 See also: `ExtendBondsData()`. 233 """ 234 235 self.calendarFile = "calendar.md" 236 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 237 238 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 239 240 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 241 """ 242 243 self.overviewFile = "overview.md" 244 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 245 246 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 247 """ 248 249 self.overviewDigestFile = "overview-digest.md" 250 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 251 252 See also: `Overview()` with parameter `details="digest"`. 253 """ 254 255 self.overviewPositionsFile = "overview-positions.md" 256 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 257 258 See also: `Overview()` with parameter `details="positions"`. 259 """ 260 261 self.overviewOrdersFile = "overview-orders.md" 262 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 263 264 See also: `Overview()` with parameter `details="orders"`. 265 """ 266 267 self.overviewAnalyticsFile = "overview-analytics.md" 268 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 269 270 See also: `Overview()` with parameter `details="analytics"`. 271 """ 272 273 self.overviewBondsCalendarFile = "overview-calendar.md" 274 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 275 276 See also: `Overview()` with parameter `details="calendar"`. 277 """ 278 279 self.reportFile = "deals.md" 280 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 281 282 See also: `Deals()`. 283 """ 284 285 self.withdrawalLimitsFile = "limits.md" 286 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 287 288 See also: `OverviewLimits()` and `RequestLimits()`. 289 """ 290 291 self.userInfoFile = "user-info.md" 292 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 293 294 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 295 """ 296 297 self.userAccountsFile = "accounts.md" 298 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 299 300 See also: `OverviewAccounts()`, `RequestAccounts()`. 301 """ 302 303 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 304 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 305 306 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 307 308 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 309 """ 310 311 self.iList = None # init iList for raw instruments data 312 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 313 314 See also: `Listing()`, `DumpInstruments()`. 315 """ 316 317 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 318 if useCache: 319 if os.path.exists(self.iListDumpFile): 320 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 321 curTime = datetime.now(tzutc()) 322 323 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 324 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 325 326 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 327 328 else: 329 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 330 331 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 332 os.path.abspath(self.iListDumpFile), 333 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 334 )) 335 336 else: 337 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 338 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 339 340 else: 341 self.iList = self.Listing() # request new raw instruments data from broker server 342 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 343 344 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 345 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 346 347 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 348 """ 349 350 def _ParseJSON(self, rawData="{}") -> dict: 351 """ 352 Parse JSON from response string. 353 354 :param rawData: this is a string with JSON-formatted text. 355 :return: JSON (dictionary), parsed from server response string. 356 """ 357 responseJSON = json.loads(rawData) if rawData else {} 358 359 if self.moreDebug: 360 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 361 362 return responseJSON 363 364 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 365 """ 366 Send GET or POST request to broker server and receive JSON object. 367 368 self.header: must be defining with dictionary of headers. 369 self.body: if define then used as request body. None by default. 370 self.timeout: global request timeout, 15 seconds by default. 371 :param url: url with REST request. 372 :param reqType: send "GET" or "POST" request. "GET" by default. 373 :param retry: how many times retry after first request if an 5xx server errors occurred. 374 :param pause: sleep time in seconds between retries. 375 :return: response JSON (dictionary) from broker. 376 """ 377 if reqType.upper() not in ("GET", "POST"): 378 uLogger.error("You can define request type: `GET` or `POST`!") 379 raise Exception("Incorrect value") 380 381 if self.moreDebug: 382 uLogger.debug("Request parameters:") 383 uLogger.debug(" - REST API URL: {}".format(url)) 384 uLogger.debug(" - request type: {}".format(reqType)) 385 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 386 uLogger.debug(" - body:\n{}".format(self.body)) 387 388 # fast hack to avoid all operations with some tickers/FIGI 389 responseJSON = {} 390 oK = True 391 for item in self.exclude: 392 if item in url: 393 if self.moreDebug: 394 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 395 396 oK = False 397 break 398 399 if oK: 400 counter = 0 401 response = None 402 errMsg = "" 403 404 while not response and counter <= retry: 405 if reqType == "GET": 406 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 407 408 if reqType == "POST": 409 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 410 411 if self.moreDebug: 412 uLogger.debug("Response:") 413 uLogger.debug(" - status code: {}".format(response.status_code)) 414 uLogger.debug(" - reason: {}".format(response.reason)) 415 uLogger.debug(" - body length: {}".format(len(response.text))) 416 uLogger.debug(" - headers:\n{}".format(response.headers)) 417 418 # Server returns some headers: 419 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 420 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 421 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 422 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 423 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 424 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 425 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 426 sleep(rateLimitWait) 427 428 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 429 if 400 <= response.status_code < 500: 430 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 431 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 432 433 if "code" in response.text and "message" in response.text: 434 msgDict = self._ParseJSON(rawData=response.text) 435 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 436 437 counter = retry + 1 # do not retry for 4xx errors 438 439 if 500 <= response.status_code < 600: 440 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 441 uLogger.debug(" - not oK, {}".format(errMsg)) 442 443 if "code" in response.text and "message" in response.text: 444 errMsgDict = self._ParseJSON(rawData=response.text) 445 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 446 447 counter += 1 448 449 if counter <= retry: 450 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 451 sleep(pause) 452 453 responseJSON = self._ParseJSON(rawData=response.text) 454 455 if errMsg: 456 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 457 uLogger.error(" - not oK, {}".format(errMsg)) 458 459 return responseJSON 460 461 def _IUpdater(self, iType: str) -> tuple: 462 """ 463 Request instrument by type from server. See available API methods for instruments: 464 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 465 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 466 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 467 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 468 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 469 470 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 471 :return: tuple with iType name and list of available instruments of current type for defined user token. 472 """ 473 result = [] 474 475 if iType in TKS_INSTRUMENTS: 476 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 477 478 # all instruments have the same body in API v2 requests: 479 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 480 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 481 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 482 483 return iType, result 484 485 def _IWrapper(self, kwargs): 486 """ 487 Wrapper runs instrument's update method `_IUpdater()`. 488 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 489 """ 490 return self._IUpdater(**kwargs) 491 492 def Listing(self) -> dict: 493 """ 494 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 495 496 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 497 """ 498 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 499 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 500 501 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 502 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 503 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 504 505 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 506 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 507 poolUpdater.close() 508 509 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 510 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 511 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 512 513 # calculate minimum price increment (step) for all instruments and set up instrument's type: 514 for iType in iList.keys(): 515 for ticker in iList[iType]: 516 iList[iType][ticker]["type"] = iType 517 518 if "minPriceIncrement" in iList[iType][ticker].keys(): 519 iList[iType][ticker]["step"] = NanoToFloat( 520 iList[iType][ticker]["minPriceIncrement"]["units"], 521 iList[iType][ticker]["minPriceIncrement"]["nano"], 522 ) 523 524 else: 525 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 526 527 return iList 528 529 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 530 """ 531 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 532 533 See also: `DumpInstruments()`, `Listing()`. 534 535 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 536 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 537 """ 538 if self.iListDumpFile is None or not self.iListDumpFile: 539 uLogger.error("Output name of dump file must be defined!") 540 raise Exception("Filename required") 541 542 if not self.iList or forceUpdate: 543 self.iList = self.Listing() 544 545 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 546 547 # Save as XLSX with separated sheets for every type of instruments: 548 with pd.ExcelWriter( 549 path=xlsxDumpFile, 550 date_format=TKS_DATE_FORMAT, 551 datetime_format=TKS_DATE_TIME_FORMAT, 552 mode="w", 553 ) as writer: 554 for iType in TKS_INSTRUMENTS: 555 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 556 df = df[sorted(df)] # sorted by column names 557 df = df.applymap( 558 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 559 na_action="ignore", 560 ) # converting numbers from nano-type to float in every cell 561 df.to_excel( 562 writer, 563 sheet_name=iType, 564 encoding="UTF-8", 565 freeze_panes=(1, 1), 566 ) # saving as XLSX-file with freeze first row and column as headers 567 568 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 569 570 def DumpInstruments(self, forceUpdate: bool = True) -> str: 571 """ 572 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 573 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 574 575 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 576 577 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 578 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 579 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 580 """ 581 if self.iListDumpFile is None or not self.iListDumpFile: 582 uLogger.error("Output name of dump file must be defined!") 583 raise Exception("Filename required") 584 585 if not self.iList or forceUpdate: 586 self.iList = self.Listing() 587 588 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 589 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 590 fH.write(jsonDump) 591 592 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 593 594 return jsonDump 595 596 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 597 """ 598 Show information about one instrument defined by json data and prints it in Markdown format. 599 600 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 601 602 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 603 :param show: if `True` then also printing information about instrument and its current price. 604 :return: multilines text in Markdown format with information about one instrument. 605 """ 606 splitLine = "| | |\n" 607 infoText = "" 608 609 if iJSON is not None and iJSON and isinstance(iJSON, dict): 610 info = [ 611 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 612 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 613 "| Parameters | Values |\n", 614 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 615 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 616 "| Full name: | {:<54} |\n".format(iJSON["name"]), 617 ] 618 619 if "sector" in iJSON.keys() and iJSON["sector"]: 620 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 621 622 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 623 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 624 625 info.extend([ 626 splitLine, 627 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 628 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 629 ]) 630 631 if "isin" in iJSON.keys() and iJSON["isin"]: 632 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 633 634 if "classCode" in iJSON.keys(): 635 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 636 637 info.extend([ 638 splitLine, 639 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 640 splitLine, 641 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 642 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 643 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 644 ]) 645 646 if iJSON["figi"]: 647 self.figi = iJSON["figi"] 648 iJSON = iJSON | self.RequestTradingStatus() 649 650 info.extend([ 651 splitLine, 652 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 653 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 654 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 655 ]) 656 657 info.append(splitLine) 658 659 if "type" in iJSON.keys() and iJSON["type"]: 660 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 661 662 if "shareType" in iJSON.keys() and iJSON["shareType"]: 663 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 664 665 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 666 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 667 668 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 669 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 670 671 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 672 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 673 674 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 675 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 676 677 if "focusType" in iJSON.keys() and iJSON["focusType"]: 678 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 679 680 if "assetType" in iJSON.keys() and iJSON["assetType"]: 681 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 682 683 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 684 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 685 686 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 687 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 688 689 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 690 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 691 692 if "currency" in iJSON.keys(): 693 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 694 695 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 696 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 697 698 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 699 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 700 701 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 702 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 703 704 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 705 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 706 707 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 708 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 709 710 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 711 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 712 713 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 714 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 715 716 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 717 info.append("| Perpetual bond: | Yes |\n") 718 719 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 720 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 721 722 iExt = None 723 if iJSON["type"] == "Bonds": 724 info.extend([ 725 splitLine, 726 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 727 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 728 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 729 iJSON["nominal"]["currency"], 730 )), 731 ]) 732 733 if "floatingCouponFlag" in iJSON.keys(): 734 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 735 736 if "amortizationFlag" in iJSON.keys(): 737 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 738 739 info.append(splitLine) 740 741 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 742 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 743 744 if iJSON["figi"]: 745 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 746 747 info.extend([ 748 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 749 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 750 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 751 ]) 752 753 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 754 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 755 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 756 iJSON["aciValue"]["currency"] 757 ))) 758 759 if "currentPrice" in iJSON.keys(): 760 info.append(splitLine) 761 762 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 763 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 764 765 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 766 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 767 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 768 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 769 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 770 771 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 772 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 773 774 info.extend([ 775 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 776 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 777 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 778 )), 779 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 780 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 781 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 782 )), 783 "| Changes between last deal price and last close | {:<54} |\n".format( 784 "{:.2f}%{}".format( 785 iJSON["currentPrice"]["changes"], 786 " ({}{:.2f} {})".format( 787 "+" if bondChangesDelta > 0 else "", 788 bondChangesDelta, 789 aciCurrency 790 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 791 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 792 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 793 currency 794 ), 795 ) 796 ), 797 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 798 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 799 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 800 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 801 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 802 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 803 )), 804 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 805 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 806 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 807 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 808 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 809 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 810 )), 811 ]) 812 813 if "lot" in iJSON.keys(): 814 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 815 816 if "step" in iJSON.keys() and iJSON["step"] != 0: 817 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 818 819 # Add bond payment calendar: 820 if iJSON["type"] == "Bonds": 821 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 822 info.extend(["\n", strCalendar]) 823 824 infoText += "".join(info) 825 826 if show: 827 uLogger.info("{}".format(infoText)) 828 829 else: 830 uLogger.debug("{}".format(infoText)) 831 832 if self.infoFile is not None: 833 with open(self.infoFile, "w", encoding="UTF-8") as fH: 834 fH.write(infoText) 835 836 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 837 838 return infoText 839 840 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 841 """ 842 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 843 844 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 845 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 846 :return: JSON formatted data with information about instrument. 847 """ 848 tickerJSON = {} 849 if self.moreDebug: 850 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 851 852 if not self.ticker: 853 uLogger.warning("self.ticker variable is not be empty!") 854 855 else: 856 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 857 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 858 raise Exception("Instrument not allowed") 859 860 if not self.iList: 861 self.iList = self.Listing() 862 863 if self.ticker in self.iList["Shares"].keys(): 864 tickerJSON = self.iList["Shares"][self.ticker] 865 if self.moreDebug: 866 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 867 868 elif self.ticker in self.iList["Currencies"].keys(): 869 tickerJSON = self.iList["Currencies"][self.ticker] 870 if self.moreDebug: 871 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 872 873 elif self.ticker in self.iList["Bonds"].keys(): 874 tickerJSON = self.iList["Bonds"][self.ticker] 875 if self.moreDebug: 876 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 877 878 elif self.ticker in self.iList["Etfs"].keys(): 879 tickerJSON = self.iList["Etfs"][self.ticker] 880 if self.moreDebug: 881 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 882 883 elif self.ticker in self.iList["Futures"].keys(): 884 tickerJSON = self.iList["Futures"][self.ticker] 885 if self.moreDebug: 886 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 887 888 if tickerJSON: 889 self.figi = tickerJSON["figi"] 890 891 if requestPrice: 892 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 893 894 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 895 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 896 897 else: 898 tickerJSON["currentPrice"]["changes"] = 0 899 900 if show: 901 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 902 903 else: 904 if show: 905 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 906 907 return tickerJSON 908 909 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 910 """ 911 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 912 913 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 914 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 915 :return: JSON formatted data with information about instrument. 916 """ 917 figiJSON = {} 918 if self.moreDebug: 919 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 920 921 if not self.figi: 922 uLogger.warning("self.figi variable is not be empty!") 923 924 else: 925 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 926 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 927 raise Exception("Instrument not allowed") 928 929 if not self.iList: 930 self.iList = self.Listing() 931 932 for item in self.iList["Shares"].keys(): 933 if self.figi == self.iList["Shares"][item]["figi"]: 934 figiJSON = self.iList["Shares"][item] 935 936 if self.moreDebug: 937 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 938 939 break 940 941 if not figiJSON: 942 for item in self.iList["Currencies"].keys(): 943 if self.figi == self.iList["Currencies"][item]["figi"]: 944 figiJSON = self.iList["Currencies"][item] 945 946 if self.moreDebug: 947 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 948 949 break 950 951 if not figiJSON: 952 for item in self.iList["Bonds"].keys(): 953 if self.figi == self.iList["Bonds"][item]["figi"]: 954 figiJSON = self.iList["Bonds"][item] 955 956 if self.moreDebug: 957 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 958 959 break 960 961 if not figiJSON: 962 for item in self.iList["Etfs"].keys(): 963 if self.figi == self.iList["Etfs"][item]["figi"]: 964 figiJSON = self.iList["Etfs"][item] 965 966 if self.moreDebug: 967 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 968 969 break 970 971 if not figiJSON: 972 for item in self.iList["Futures"].keys(): 973 if self.figi == self.iList["Futures"][item]["figi"]: 974 figiJSON = self.iList["Futures"][item] 975 976 if self.moreDebug: 977 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 978 979 break 980 981 if figiJSON: 982 self.figi = figiJSON["figi"] 983 self.ticker = figiJSON["ticker"] 984 985 if requestPrice: 986 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 987 988 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 989 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 990 991 else: 992 figiJSON["currentPrice"]["changes"] = 0 993 994 if show: 995 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 996 997 else: 998 if show: 999 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1000 1001 return figiJSON 1002 1003 def GetCurrentPrices(self, show: bool = True) -> dict: 1004 """ 1005 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1006 `{"buy": [{"price": 1243.8, "quantity": 193}, 1007 {"price": 1244.0, "quantity": 168}, 1008 {"price": 1244.8, "quantity": 5}, 1009 {"price": 1245.0, "quantity": 61}, 1010 {"price": 1245.4, "quantity": 60}], 1011 "sell": [{"price": 1243.6, "quantity": 8}, 1012 {"price": 1242.6, "quantity": 10}, 1013 {"price": 1242.4, "quantity": 18}, 1014 {"price": 1242.2, "quantity": 50}, 1015 {"price": 1242.0, "quantity": 113}], 1016 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1017 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1018 - sell: list of dicts with Buyers prices, 1019 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1020 - quantity: volume value by current price in lots, 1021 - limitUp: current trade session limit price, maximum, 1022 - limitDown: current trade session limit price, minimum, 1023 - lastPrice: last deal price of the instrument, 1024 - closePrice: previous trade session close price of the instrument. 1025 1026 See also: `SearchByTicker()` and `SearchByFIGI()`. 1027 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1028 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1029 1030 :param show: if `True` then print DOM to log and console. 1031 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1032 If an error occurred then returns an empty record: 1033 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1034 """ 1035 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1036 1037 if self.depth < 1: 1038 uLogger.error("Depth of Market (DOM) must be >=1!") 1039 raise Exception("Incorrect value") 1040 1041 if not (self.ticker or self.figi): 1042 uLogger.error("self.ticker or self.figi variables must be defined!") 1043 raise Exception("Ticker or FIGI required") 1044 1045 if self.ticker and not self.figi: 1046 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1047 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1048 1049 if not self.ticker and self.figi: 1050 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1051 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1052 1053 if not self.figi: 1054 uLogger.error("FIGI is not defined!") 1055 raise Exception("Ticker or FIGI required") 1056 1057 else: 1058 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1059 1060 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1061 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1062 self.body = str({"figi": self.figi, "depth": self.depth}) 1063 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1064 1065 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1066 # list of dicts with sellers orders: 1067 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1068 1069 # list of dicts with buyers orders: 1070 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1071 1072 # max price of instrument at this time: 1073 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1074 1075 # min price of instrument at this time: 1076 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1077 1078 # last price of deal with instrument: 1079 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1080 1081 # last close price of instrument: 1082 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1083 1084 else: 1085 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1086 uLogger.debug("Server response: {}".format(pricesResponse)) 1087 1088 if show: 1089 if prices["buy"] or prices["sell"]: 1090 info = [ 1091 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1092 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1093 self.ticker, 1094 self.figi, 1095 self.depth, 1096 ), 1097 "-" * 60, "\n", 1098 " Orders of Buyers | Orders of Sellers\n", 1099 "-" * 60, "\n", 1100 " Sell prices (volumes) | Buy prices (volumes)\n", 1101 "-" * 60, "\n", 1102 ] 1103 1104 if not prices["buy"]: 1105 info.append(" | No orders!\n") 1106 sumBuy = 0 1107 1108 else: 1109 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1110 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1111 for item in maxMinSorted: 1112 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1113 1114 if not prices["sell"]: 1115 info.append("No orders! |\n") 1116 sumSell = 0 1117 1118 else: 1119 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1120 for item in prices["sell"]: 1121 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1122 1123 info.extend([ 1124 "-" * 60, "\n", 1125 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1126 "-" * 60, "\n", 1127 ]) 1128 1129 infoText = "".join(info) 1130 1131 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1132 1133 else: 1134 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1135 1136 return prices 1137 1138 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1139 """ 1140 This method get and show information about all available broker instruments for current user account. 1141 If `instrumentsFile` string is not empty then also save information to this file. 1142 1143 :param show: if `True` then print results to console, if `False` — print only to file. 1144 :return: multi-lines string with all available broker instruments 1145 """ 1146 if not self.iList: 1147 self.iList = self.Listing() 1148 1149 info = [ 1150 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1151 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1152 ] 1153 1154 # add instruments count by type: 1155 for iType in self.iList.keys(): 1156 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1157 1158 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1159 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1160 1161 # generating info tables with all instruments by type: 1162 for iType in self.iList.keys(): 1163 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1164 1165 for instrument in self.iList[iType].keys(): 1166 iName = self.iList[iType][instrument]["name"] # instrument's name 1167 if len(iName) > 57: 1168 iName = "{}...".format(iName[:54]) # right trim for a long string 1169 1170 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1171 self.iList[iType][instrument]["ticker"], 1172 iName, 1173 self.iList[iType][instrument]["figi"], 1174 self.iList[iType][instrument]["currency"], 1175 self.iList[iType][instrument]["lot"], 1176 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1177 )) 1178 1179 infoText = "".join(info) 1180 1181 if show: 1182 uLogger.info(infoText) 1183 1184 if self.instrumentsFile: 1185 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1186 fH.write(infoText) 1187 1188 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1189 1190 return infoText 1191 1192 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1193 """ 1194 This method search and show information about instruments by part of its ticker, FIGI or name. 1195 If `searchResultsFile` string is not empty then also save information to this file. 1196 1197 :param pattern: string with part of ticker, FIGI or instrument's name. 1198 :param show: if `True` then print results to console, if `False` — return list of result only. 1199 :return: list of dictionaries with all found instruments. 1200 """ 1201 if not self.iList: 1202 self.iList = self.Listing() 1203 1204 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1205 compiledPattern = re.compile(pattern, re.IGNORECASE) 1206 1207 for iType in self.iList: 1208 for instrument in self.iList[iType].values(): 1209 searchResult = compiledPattern.search(" ".join( 1210 [instrument["ticker"], instrument["figi"], instrument["name"]] 1211 )) 1212 1213 if searchResult: 1214 searchResults[iType][instrument["ticker"]] = instrument 1215 1216 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1217 info = [ 1218 "# Search results\n\n", 1219 "* **Search pattern:** [{}]\n".format(pattern), 1220 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1221 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1222 ] 1223 infoShort = info[:] 1224 1225 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1226 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1227 skippedLine = "| ... | ... | ... | ... |\n" 1228 1229 if resultsLen == 0: 1230 info.append("\nNo results\n") 1231 infoShort.append("\nNo results\n") 1232 uLogger.warning("No results. Try changing your search pattern.") 1233 1234 else: 1235 for iType in searchResults: 1236 iTypeValuesCount = len(searchResults[iType].values()) 1237 if iTypeValuesCount > 0: 1238 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1239 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1240 1241 for instrument in searchResults[iType].values(): 1242 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1243 instrument["type"], 1244 instrument["ticker"], 1245 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1246 instrument["figi"], 1247 )) 1248 1249 if iTypeValuesCount <= 5: 1250 infoShort.extend(info[-iTypeValuesCount:]) 1251 1252 else: 1253 infoShort.extend(info[-5:]) 1254 infoShort.append(skippedLine) 1255 1256 infoText = "".join(info) 1257 infoTextShort = "".join(infoShort) 1258 1259 if show: 1260 uLogger.info(infoTextShort) 1261 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1262 1263 if self.searchResultsFile: 1264 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1265 fH.write(infoText) 1266 1267 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1268 1269 return searchResults 1270 1271 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1272 """ 1273 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1274 1275 :param instruments: list of strings with tickers or FIGIs. 1276 :return: list with unique instrument FIGIs only. 1277 """ 1278 requestedInstruments = [] 1279 for iName in instruments: 1280 if iName not in self.aliases.keys(): 1281 if iName not in requestedInstruments: 1282 requestedInstruments.append(iName) 1283 1284 else: 1285 if iName not in requestedInstruments: 1286 if self.aliases[iName] not in requestedInstruments: 1287 requestedInstruments.append(self.aliases[iName]) 1288 1289 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1290 1291 onlyUniqueFIGIs = [] 1292 for iName in requestedInstruments: 1293 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1294 continue 1295 1296 self.ticker = iName 1297 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1298 1299 if not iData: 1300 self.ticker = "" 1301 self.figi = iName 1302 1303 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1304 1305 if not iData: 1306 self.figi = "" 1307 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1308 1309 if iData and iData["figi"] not in onlyUniqueFIGIs: 1310 onlyUniqueFIGIs.append(iData["figi"]) 1311 1312 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1313 1314 return onlyUniqueFIGIs 1315 1316 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1317 """ 1318 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1319 1320 See limits: https://tinkoff.github.io/investAPI/limits/ 1321 1322 If `pricesFile` string is not empty then also save information to this file. 1323 1324 :param instruments: list of strings with tickers or FIGIs. 1325 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1326 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1327 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1328 """ 1329 if instruments is None or not instruments: 1330 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1331 raise Exception("Ticker or FIGI required") 1332 1333 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1334 1335 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1336 1337 iList = [] # trying to get info and current prices about all unique instruments: 1338 for self.figi in onlyUniqueFIGIs: 1339 iData = self.SearchByFIGI(requestPrice=True) 1340 iList.append(iData) 1341 1342 self.ShowListOfPrices(iList, show) 1343 1344 return iList 1345 1346 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1347 """ 1348 Show table contains current prices of given instruments. 1349 1350 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1351 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1352 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1353 :return: multilines text in Markdown format as a table contains current prices. 1354 """ 1355 infoText = "" 1356 1357 if show or self.pricesFile: 1358 info = [ 1359 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1360 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1361 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1362 ] 1363 1364 for item in iList: 1365 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1366 item["ticker"], 1367 item["figi"], 1368 item["type"], 1369 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1370 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1371 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1372 "{} / {}".format( 1373 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1374 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1375 ), 1376 "{} / {}".format( 1377 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1378 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1379 ), 1380 item["currency"], 1381 )) 1382 1383 infoText = "".join(info) 1384 1385 if show: 1386 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1387 1388 if self.pricesFile: 1389 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1390 fH.write(infoText) 1391 1392 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1393 1394 return infoText 1395 1396 def RequestTradingStatus(self) -> dict: 1397 """ 1398 Requesting trading status for the instrument defined by `figi` variable. 1399 1400 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1401 1402 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1403 1404 :return: dictionary with trading status attributes. Response example: 1405 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1406 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1407 """ 1408 if self.figi is None or not self.figi: 1409 uLogger.error("Variable `figi` must be defined for using this method!") 1410 raise Exception("FIGI required") 1411 1412 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1413 1414 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1415 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1416 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1417 1418 if self.moreDebug: 1419 uLogger.debug("Records about current trading status successfully received") 1420 1421 return tradingStatus 1422 1423 def RequestPortfolio(self) -> dict: 1424 """ 1425 Requesting actual user's portfolio for current `accountId`. 1426 1427 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1428 1429 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1430 1431 :return: dictionary with user's portfolio. 1432 """ 1433 if self.accountId is None or not self.accountId: 1434 uLogger.error("Variable `accountId` must be defined for using this method!") 1435 raise Exception("Account ID required") 1436 1437 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1438 1439 self.body = str({"accountId": self.accountId}) 1440 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1441 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1442 1443 if self.moreDebug: 1444 uLogger.debug("Records about user's portfolio successfully received") 1445 1446 return rawPortfolio 1447 1448 def RequestPositions(self) -> dict: 1449 """ 1450 Requesting open positions by currencies and instruments for current `accountId`. 1451 1452 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1453 1454 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1455 1456 :return: dictionary with open positions by instruments. 1457 """ 1458 if self.accountId is None or not self.accountId: 1459 uLogger.error("Variable `accountId` must be defined for using this method!") 1460 raise Exception("Account ID required") 1461 1462 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1463 1464 self.body = str({"accountId": self.accountId}) 1465 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1466 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1467 1468 if self.moreDebug: 1469 uLogger.debug("Records about current open positions successfully received") 1470 1471 return rawPositions 1472 1473 def RequestPendingOrders(self) -> list: 1474 """ 1475 Requesting current actual pending limit orders for current `accountId`. 1476 1477 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1478 1479 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1480 1481 :return: list of dictionaries with pending limit orders. 1482 """ 1483 if self.accountId is None or not self.accountId: 1484 uLogger.error("Variable `accountId` must be defined for using this method!") 1485 raise Exception("Account ID required") 1486 1487 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1488 1489 self.body = str({"accountId": self.accountId}) 1490 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1491 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1492 1493 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1494 1495 return rawOrders 1496 1497 def RequestStopOrders(self) -> list: 1498 """ 1499 Requesting current actual stop orders for current `accountId`. 1500 1501 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1502 1503 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1504 1505 :return: list of dictionaries with stop orders. 1506 """ 1507 if self.accountId is None or not self.accountId: 1508 uLogger.error("Variable `accountId` must be defined for using this method!") 1509 raise Exception("Account ID required") 1510 1511 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1512 1513 self.body = str({"accountId": self.accountId}) 1514 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1515 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1516 1517 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1518 1519 return rawStopOrders 1520 1521 def Overview(self, show: bool = False, details: str = "full") -> dict: 1522 """ 1523 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1524 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1525 and `overviewBondsCalendarFile` are defined then also save information to file. 1526 1527 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1528 many requests about the state of the portfolio, and then, based on the received data, a large number 1529 of calculation and statistics are collected. 1530 1531 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1532 :param details: how detailed should the information be? 1533 - `full` — shows full available information about portfolio status (by default), 1534 - `positions` — shows only open positions, 1535 - `orders` — shows only sections of open limits and stop orders. 1536 - `digest` — show a short digest of the portfolio status, 1537 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1538 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1539 :return: dictionary with client's raw portfolio and some statistics. 1540 """ 1541 if self.accountId is None or not self.accountId: 1542 uLogger.error("Variable `accountId` must be defined for using this method!") 1543 raise Exception("Account ID required") 1544 1545 view = { 1546 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1547 "headers": {}, # list of dictionaries, response headers without "positions" section 1548 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1549 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1550 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1551 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1552 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1553 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1554 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1555 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1556 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1557 }, 1558 "stat": { # --- some statistics calculated using "raw" sections: 1559 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1560 "availableRUB": 0., # available rubles (without other currencies) 1561 "blockedRUB": 0., # blocked sum in Russian Rouble 1562 "totalChangesRUB": 0., # changes for all open trades in RUB 1563 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1564 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1565 "sharesCostRUB": 0., # costs of all shares in RUB 1566 "bondsCostRUB": 0., # costs of all bonds in RUB 1567 "etfsCostRUB": 0., # costs of all etfs in RUB 1568 "futuresCostRUB": 0., # costs of all futures in RUB 1569 "Currencies": [], # list of dictionaries of all currencies statistics 1570 "Shares": [], # list of dictionaries of all shares statistics 1571 "Bonds": [], # list of dictionaries of all bonds statistics 1572 "Etfs": [], # list of dictionaries of all etfs statistics 1573 "Futures": [], # list of dictionaries of all futures statistics 1574 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1575 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1576 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1577 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1578 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1579 }, 1580 "analytics": { # --- some analytics of portfolio: 1581 "distrByAssets": {}, # portfolio distribution by assets 1582 "distrByCompanies": {}, # portfolio distribution by companies 1583 "distrBySectors": {}, # portfolio distribution by sectors 1584 "distrByCurrencies": {}, # portfolio distribution by currencies 1585 "distrByCountries": {}, # portfolio distribution by countries 1586 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1587 } 1588 } 1589 1590 details = details.lower() 1591 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1592 if details not in availableDetails: 1593 details = "full" 1594 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1595 1596 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1597 1598 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1599 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1600 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1601 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1602 1603 # save response headers without "positions" section: 1604 for key in portfolioResponse.keys(): 1605 if key != "positions": 1606 view["raw"]["headers"][key] = portfolioResponse[key] 1607 1608 else: 1609 continue 1610 1611 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1612 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1613 for item in portfolioResponse["positions"]: 1614 if item["instrumentType"] == "currency": 1615 self.figi = item["figi"] 1616 curr = self.SearchByFIGI(requestPrice=False) 1617 1618 # current price of currency in RUB: 1619 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1620 "name": curr["name"], 1621 "currentPrice": NanoToFloat( 1622 item["currentPrice"]["units"], 1623 item["currentPrice"]["nano"] 1624 ), 1625 } 1626 1627 view["raw"]["Currencies"].append(item) 1628 1629 elif item["instrumentType"] == "share": 1630 view["raw"]["Shares"].append(item) 1631 1632 elif item["instrumentType"] == "bond": 1633 view["raw"]["Bonds"].append(item) 1634 1635 elif item["instrumentType"] == "etf": 1636 view["raw"]["Etfs"].append(item) 1637 1638 elif item["instrumentType"] == "futures": 1639 view["raw"]["Futures"].append(item) 1640 1641 else: 1642 continue 1643 1644 # how many volume of currencies (by ISO currency name) are blocked: 1645 for item in view["raw"]["positions"]["blocked"]: 1646 blocked = NanoToFloat(item["units"], item["nano"]) 1647 if blocked > 0: 1648 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1649 1650 # how many volume of instruments (by FIGI) are blocked: 1651 for item in view["raw"]["positions"]["securities"]: 1652 blocked = int(item["blocked"]) 1653 if blocked > 0: 1654 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1655 1656 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1657 1658 if "rub" in allBlocked.keys(): 1659 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1660 1661 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1662 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1663 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1664 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1665 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1666 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1667 view["stat"]["portfolioCostRUB"] = sum([ 1668 view["stat"]["allCurrenciesCostRUB"], 1669 view["stat"]["sharesCostRUB"], 1670 view["stat"]["bondsCostRUB"], 1671 view["stat"]["etfsCostRUB"], 1672 view["stat"]["futuresCostRUB"], 1673 ]) 1674 1675 # --- calculating some portfolio statistics: 1676 byComp = {} # distribution by companies 1677 bySect = {} # distribution by sectors 1678 byCurr = {} # distribution by currencies (include RUB) 1679 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1680 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1681 1682 for item in portfolioResponse["positions"]: 1683 self.figi = item["figi"] 1684 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1685 1686 if instrument: 1687 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1688 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1689 1690 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1691 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1692 1693 else: 1694 blocked = 0 1695 1696 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1697 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1698 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1699 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1700 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1701 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1702 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1703 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1704 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1705 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1706 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1707 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1708 1709 statData = { 1710 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1711 "ticker": instrument["ticker"], # ticker by FIGI 1712 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1713 "volume": volume, # available volume of instrument 1714 "lots": lots, # volume in lots of instrument 1715 "direction": direction, # direction of an instrument's position: short or long 1716 "blocked": blocked, # blocked volume of currency or instrument 1717 "currentPrice": curPrice, # current instrument's price in basic asset 1718 "average": average, # current average position price 1719 "cost": cost, # current cost of all volume of instrument in basic asset 1720 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1721 "costRUB": costRUB, # cost of instrument in ruble 1722 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1723 "profit": profit, # expected profit at current moment 1724 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1725 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1726 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1727 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1728 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1729 "step": instrument["step"], # minimum price increment 1730 } 1731 1732 # adding distribution by unique countries: 1733 if statData["country"] not in byCountry.keys(): 1734 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1735 1736 else: 1737 byCountry[statData["country"]]["cost"] += costRUB 1738 byCountry[statData["country"]]["percent"] += percentCostRUB 1739 1740 if item["instrumentType"] != "currency": 1741 # adding distribution by unique companies: 1742 if statData["name"]: 1743 if statData["name"] not in byComp.keys(): 1744 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1745 1746 else: 1747 byComp[statData["name"]]["cost"] += costRUB 1748 byComp[statData["name"]]["percent"] += percentCostRUB 1749 1750 # adding distribution by unique sectors: 1751 if statData["sector"] not in bySect.keys(): 1752 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1753 1754 else: 1755 bySect[statData["sector"]]["cost"] += costRUB 1756 bySect[statData["sector"]]["percent"] += percentCostRUB 1757 1758 # adding distribution by unique currencies: 1759 if currency not in byCurr.keys(): 1760 byCurr[currency] = { 1761 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1762 "cost": costRUB, 1763 "percent": percentCostRUB 1764 } 1765 1766 else: 1767 byCurr[currency]["cost"] += costRUB 1768 byCurr[currency]["percent"] += percentCostRUB 1769 1770 # saving statistics for every instrument: 1771 if item["instrumentType"] == "currency": 1772 view["stat"]["Currencies"].append(statData) 1773 1774 # update dict with free funds for trading (total - blocked) by currencies 1775 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1776 view["stat"]["funds"][currency] = { 1777 "total": volume, 1778 "totalCostRUB": costRUB, # total volume cost in rubles 1779 "free": volume - blocked, 1780 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1781 } 1782 1783 elif item["instrumentType"] == "share": 1784 view["stat"]["Shares"].append(statData) 1785 1786 elif item["instrumentType"] == "bond": 1787 view["stat"]["Bonds"].append(statData) 1788 1789 elif item["instrumentType"] == "etf": 1790 view["stat"]["Etfs"].append(statData) 1791 1792 elif item["instrumentType"] == "Futures": 1793 view["stat"]["Futures"].append(statData) 1794 1795 else: 1796 continue 1797 1798 # total changes in Russian Ruble: 1799 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1800 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1801 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1802 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1803 view["stat"]["funds"]["rub"] = { 1804 "total": view["stat"]["availableRUB"], 1805 "totalCostRUB": view["stat"]["availableRUB"], 1806 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1807 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1808 } 1809 1810 # --- pending limit orders sector data: 1811 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1812 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1813 1814 for item in view["raw"]["orders"]: 1815 self.figi = item["figi"] 1816 1817 if item["figi"] not in uniquePendingOrdersFIGIs: 1818 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1819 1820 uniquePendingOrdersFIGIs.append(item["figi"]) 1821 uniquePendingOrders[item["figi"]] = instrument 1822 1823 else: 1824 instrument = uniquePendingOrders[item["figi"]] 1825 1826 if instrument: 1827 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1828 orderType = TKS_ORDER_TYPES[item["orderType"]] 1829 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1830 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1831 1832 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1833 if item["direction"] == "ORDER_DIRECTION_BUY": 1834 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1835 1836 else: 1837 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1838 1839 # requested price for order execution: 1840 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1841 1842 # necessary changes in percent to reach target from current price: 1843 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1844 1845 view["stat"]["orders"].append({ 1846 "orderID": item["orderId"], # orderId number parameter of current order 1847 "figi": item["figi"], # FIGI identification 1848 "ticker": instrument["ticker"], # ticker name by FIGI 1849 "lotsRequested": item["lotsRequested"], # requested lots value 1850 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1851 "currentPrice": lastPrice, # current instrument's price for defined action 1852 "targetPrice": target, # requested price for order execution in base currency 1853 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1854 "percentChanges": changes, # changes in percent to target from current price 1855 "currency": item["currency"], # instrument's currency name 1856 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1857 "type": orderType, # type of order from TKS_ORDER_TYPES 1858 "status": orderState, # order status from TKS_ORDER_STATES 1859 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1860 }) 1861 1862 # --- stop orders sector data: 1863 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1864 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1865 1866 for item in view["raw"]["stopOrders"]: 1867 self.figi = item["figi"] 1868 1869 if item["figi"] not in uniqueStopOrdersFIGIs: 1870 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1871 1872 uniqueStopOrdersFIGIs.append(item["figi"]) 1873 uniqueStopOrders[item["figi"]] = instrument 1874 1875 else: 1876 instrument = uniqueStopOrders[item["figi"]] 1877 1878 if instrument: 1879 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1880 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1881 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1882 1883 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1884 if "expirationTime" in item.keys(): 1885 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1886 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1887 1888 else: 1889 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1890 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1891 1892 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1893 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1894 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1895 1896 else: 1897 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1898 1899 # requested price when stop-order executed: 1900 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1901 1902 # price for limit-order, set up when stop-order executed: 1903 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1904 1905 # necessary changes in percent to reach target from current price: 1906 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1907 1908 view["stat"]["stopOrders"].append({ 1909 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1910 "figi": item["figi"], # FIGI identification 1911 "ticker": instrument["ticker"], # ticker name by FIGI 1912 "lotsRequested": item["lotsRequested"], # requested lots value 1913 "currentPrice": lastPrice, # current instrument's price for defined action 1914 "targetPrice": target, # requested price for stop-order execution in base currency 1915 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1916 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1917 "percentChanges": changes, # changes in percent to target from current price 1918 "currency": item["currency"], # instrument's currency name 1919 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1920 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1921 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1922 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1923 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1924 }) 1925 1926 # --- calculating data for analytics section: 1927 # portfolio distribution by assets: 1928 view["analytics"]["distrByAssets"] = { 1929 "Ruble": { 1930 "uniques": 1, 1931 "cost": view["stat"]["availableRUB"], 1932 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1933 }, 1934 "Currencies": { 1935 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1936 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1937 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1938 }, 1939 "Shares": { 1940 "uniques": len(view["stat"]["Shares"]), 1941 "cost": view["stat"]["sharesCostRUB"], 1942 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1943 }, 1944 "Bonds": { 1945 "uniques": len(view["stat"]["Bonds"]), 1946 "cost": view["stat"]["bondsCostRUB"], 1947 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1948 }, 1949 "Etfs": { 1950 "uniques": len(view["stat"]["Etfs"]), 1951 "cost": view["stat"]["etfsCostRUB"], 1952 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1953 }, 1954 "Futures": { 1955 "uniques": len(view["stat"]["Futures"]), 1956 "cost": view["stat"]["futuresCostRUB"], 1957 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1958 }, 1959 } 1960 1961 # portfolio distribution by companies: 1962 view["analytics"]["distrByCompanies"]["All money cash"] = { 1963 "ticker": "", 1964 "cost": view["stat"]["allCurrenciesCostRUB"], 1965 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1966 } 1967 view["analytics"]["distrByCompanies"].update(byComp) 1968 1969 # portfolio distribution by sectors: 1970 view["analytics"]["distrBySectors"]["All money cash"] = { 1971 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 1972 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 1973 } 1974 view["analytics"]["distrBySectors"].update(bySect) 1975 1976 # portfolio distribution by currencies: 1977 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 1978 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 1979 1980 if self.moreDebug: 1981 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 1982 1983 view["analytics"]["distrByCurrencies"].update(byCurr) 1984 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1985 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1986 1987 # portfolio distribution by countries: 1988 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 1989 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 1990 1991 if self.moreDebug: 1992 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 1993 1994 view["analytics"]["distrByCountries"].update(byCountry) 1995 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1996 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1997 1998 # --- Prepare text statistics overview in human-readable: 1999 if show: 2000 # Whatever the value `details`, header not changes: 2001 info = [ 2002 "# Client's portfolio\n\n", 2003 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2004 "* **Account ID:** [{}]\n".format(self.accountId), 2005 ] 2006 2007 if details in ["full", "positions", "digest"]: 2008 info.extend([ 2009 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2010 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2011 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2012 view["stat"]["totalChangesRUB"], 2013 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2014 view["stat"]["totalChangesPercentRUB"], 2015 ), 2016 ]) 2017 2018 if details in ["full", "positions"]: 2019 info.extend([ 2020 "## Open positions\n\n", 2021 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2022 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2023 "| Ruble | {:>31} | | | | | |\n".format( 2024 "{:.2f} ({:.2f}) rub".format( 2025 view["stat"]["availableRUB"], 2026 view["stat"]["blockedRUB"], 2027 ) 2028 ) 2029 ]) 2030 2031 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2032 return [ 2033 "| | | | | | | |\n", 2034 "| {:<27} | | | | | {:>19} | |\n".format( 2035 noTradeStr if noTradeStr else typeStr, 2036 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2037 ), 2038 ] 2039 2040 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2041 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2042 "{} [{}]".format(data["ticker"], data["figi"]), 2043 "{:.2f} ({:.2f}) {}".format( 2044 data["volume"], 2045 data["blocked"], 2046 data["currency"], 2047 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2048 data["volume"], 2049 data["blocked"], 2050 ), 2051 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2052 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2053 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2054 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2055 "{}{:.2f} {} ({}{:.2f}%)".format( 2056 "+" if data["profit"] > 0 else "", 2057 data["profit"], data["baseCurrencyName"], 2058 "+" if data["percentProfit"] > 0 else "", 2059 data["percentProfit"], 2060 ), 2061 ) 2062 2063 # --- Show currencies section: 2064 if view["stat"]["Currencies"]: 2065 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2066 for item in view["stat"]["Currencies"]: 2067 info.append(_InfoStr(item, showCurrencyName=True)) 2068 2069 else: 2070 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2071 2072 # --- Show shares section: 2073 if view["stat"]["Shares"]: 2074 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2075 2076 for item in view["stat"]["Shares"]: 2077 info.append(_InfoStr(item)) 2078 2079 else: 2080 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2081 2082 # --- Show bonds section: 2083 if view["stat"]["Bonds"]: 2084 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2085 2086 for item in view["stat"]["Bonds"]: 2087 info.append(_InfoStr(item)) 2088 2089 else: 2090 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2091 2092 # --- Show etfs section: 2093 if view["stat"]["Etfs"]: 2094 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2095 2096 for item in view["stat"]["Etfs"]: 2097 info.append(_InfoStr(item)) 2098 2099 else: 2100 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2101 2102 # --- Show futures section: 2103 if view["stat"]["Futures"]: 2104 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2105 2106 for item in view["stat"]["Futures"]: 2107 info.append(_InfoStr(item)) 2108 2109 else: 2110 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2111 2112 if details in ["full", "orders"]: 2113 # --- Show pending limit orders section: 2114 if view["stat"]["orders"]: 2115 info.extend([ 2116 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2117 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2118 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2119 ]) 2120 2121 for item in view["stat"]["orders"]: 2122 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2123 "{} [{}]".format(item["ticker"], item["figi"]), 2124 item["orderID"], 2125 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2126 "{} {} ({}{:.2f}%)".format( 2127 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2128 item["baseCurrencyName"], 2129 "+" if item["percentChanges"] > 0 else "", 2130 float(item["percentChanges"]), 2131 ), 2132 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2133 item["action"], 2134 item["type"], 2135 item["date"], 2136 )) 2137 2138 else: 2139 info.append("\n## Total pending limit-orders: 0\n") 2140 2141 # --- Show stop orders section: 2142 if view["stat"]["stopOrders"]: 2143 info.extend([ 2144 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2145 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2146 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2147 ]) 2148 2149 for item in view["stat"]["stopOrders"]: 2150 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2151 "{} [{}]".format(item["ticker"], item["figi"]), 2152 item["orderID"], 2153 item["lotsRequested"], 2154 "{} {} ({}{:.2f}%)".format( 2155 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2156 item["baseCurrencyName"], 2157 "+" if item["percentChanges"] > 0 else "", 2158 float(item["percentChanges"]), 2159 ), 2160 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2161 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2162 item["action"], 2163 item["type"], 2164 item["expType"], 2165 item["createDate"], 2166 item["expDate"], 2167 )) 2168 2169 else: 2170 info.append("\n## Total stop-orders: 0\n") 2171 2172 if details in ["full", "analytics"]: 2173 # -- Show analytics section: 2174 if view["stat"]["portfolioCostRUB"] > 0: 2175 info.extend([ 2176 "\n# Analytics\n" 2177 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2178 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2179 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2180 view["stat"]["totalChangesRUB"], 2181 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2182 view["stat"]["totalChangesPercentRUB"], 2183 ), 2184 "\n## Portfolio distribution by assets\n" 2185 "\n| Type | Uniques | Percent | Current cost |\n", 2186 "|------------------------------------|---------|---------|--------------------|\n", 2187 ]) 2188 2189 for key in view["analytics"]["distrByAssets"].keys(): 2190 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2191 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2192 key, 2193 view["analytics"]["distrByAssets"][key]["uniques"], 2194 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2195 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2196 )) 2197 2198 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2199 2200 info.extend([ 2201 "\n## Portfolio distribution by companies\n" 2202 "\n| Company | Percent | Current cost |\n", 2203 aSepLine, 2204 ]) 2205 2206 for company in view["analytics"]["distrByCompanies"].keys(): 2207 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2208 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2209 "{}{}".format( 2210 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2211 company, 2212 ), 2213 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2214 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2215 )) 2216 2217 info.extend([ 2218 "\n## Portfolio distribution by sectors\n" 2219 "\n| Sector | Percent | Current cost |\n", 2220 aSepLine, 2221 ]) 2222 2223 for sector in view["analytics"]["distrBySectors"].keys(): 2224 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2225 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2226 sector, 2227 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2228 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2229 )) 2230 2231 info.extend([ 2232 "\n## Portfolio distribution by currencies\n" 2233 "\n| Instruments currencies | Percent | Current cost |\n", 2234 aSepLine, 2235 ]) 2236 2237 for curr in view["analytics"]["distrByCurrencies"].keys(): 2238 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2239 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2240 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2241 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2242 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2243 )) 2244 2245 info.extend([ 2246 "\n## Portfolio distribution by countries\n" 2247 "\n| Assets by country | Percent | Current cost |\n", 2248 aSepLine, 2249 ]) 2250 2251 for country in view["analytics"]["distrByCountries"].keys(): 2252 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2253 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2254 country, 2255 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2256 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2257 )) 2258 2259 if details in ["full", "calendar"]: 2260 # -- Show bonds payment calendar section: 2261 if view["stat"]["Bonds"]: 2262 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2263 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2264 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2265 2266 else: 2267 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2268 2269 infoText = "".join(info) 2270 2271 uLogger.info(infoText) 2272 2273 if details == "full" and self.overviewFile: 2274 filename = self.overviewFile 2275 2276 elif details == "digest" and self.overviewDigestFile: 2277 filename = self.overviewDigestFile 2278 2279 elif details == "positions" and self.overviewPositionsFile: 2280 filename = self.overviewPositionsFile 2281 2282 elif details == "orders" and self.overviewOrdersFile: 2283 filename = self.overviewOrdersFile 2284 2285 elif details == "analytics" and self.overviewAnalyticsFile: 2286 filename = self.overviewAnalyticsFile 2287 2288 elif details == "calendar" and self.overviewBondsCalendarFile: 2289 filename = self.overviewBondsCalendarFile 2290 2291 else: 2292 filename = "" 2293 2294 if filename: 2295 with open(filename, "w", encoding="UTF-8") as fH: 2296 fH.write(infoText) 2297 2298 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2299 2300 return view 2301 2302 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2303 """ 2304 Returns history operations between two given dates for current `accountId`. 2305 If `reportFile` string is not empty then also save human-readable report. 2306 Shows some statistical data of closed positions. 2307 2308 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2309 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2310 :param show: if `True` then also prints all records to the console. 2311 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2312 :return: original list of dictionaries with history of deals records from API ("operations" key): 2313 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2314 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2315 """ 2316 if self.accountId is None or not self.accountId: 2317 uLogger.error("Variable `accountId` must be defined for using this method!") 2318 raise Exception("Account ID required") 2319 2320 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2321 2322 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2323 2324 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2325 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2326 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2327 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2328 customStat = {} # custom statistics in additional to responseJSON 2329 2330 # --- output report in human-readable format: 2331 if show or self.reportFile: 2332 splitLine1 = "| | | | | |\n" # Summary section 2333 splitLine2 = "| | | | | | | | |\n" # Operations section 2334 nextDay = "" 2335 2336 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2337 2338 if len(ops) > 0: 2339 customStat = { 2340 "opsCount": 0, # total operations count 2341 "buyCount": 0, # buy operations 2342 "sellCount": 0, # sell operations 2343 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2344 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2345 "payIn": {"rub": 0.}, # Deposit brokerage account 2346 "payOut": {"rub": 0.}, # Withdrawals 2347 "divs": {"rub": 0.}, # Dividends income 2348 "coupons": {"rub": 0.}, # Coupon's income 2349 "brokerCom": {"rub": 0.}, # Service commissions 2350 "serviceCom": {"rub": 0.}, # Service commissions 2351 "marginCom": {"rub": 0.}, # Margin commissions 2352 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2353 } 2354 2355 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2356 for item in ops: 2357 if item["state"] == "OPERATION_STATE_EXECUTED": 2358 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2359 2360 # count buy operations: 2361 if "_BUY" in item["operationType"]: 2362 customStat["buyCount"] += 1 2363 2364 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2365 customStat["buyTotal"][item["payment"]["currency"]] += payment 2366 2367 else: 2368 customStat["buyTotal"][item["payment"]["currency"]] = payment 2369 2370 # count sell operations: 2371 elif "_SELL" in item["operationType"]: 2372 customStat["sellCount"] += 1 2373 2374 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2375 customStat["sellTotal"][item["payment"]["currency"]] += payment 2376 2377 else: 2378 customStat["sellTotal"][item["payment"]["currency"]] = payment 2379 2380 # count incoming operations: 2381 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2382 if item["payment"]["currency"] in customStat["payIn"].keys(): 2383 customStat["payIn"][item["payment"]["currency"]] += payment 2384 2385 else: 2386 customStat["payIn"][item["payment"]["currency"]] = payment 2387 2388 # count withdrawals operations: 2389 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2390 if item["payment"]["currency"] in customStat["payOut"].keys(): 2391 customStat["payOut"][item["payment"]["currency"]] += payment 2392 2393 else: 2394 customStat["payOut"][item["payment"]["currency"]] = payment 2395 2396 # count dividends income: 2397 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2398 if item["payment"]["currency"] in customStat["divs"].keys(): 2399 customStat["divs"][item["payment"]["currency"]] += payment 2400 2401 else: 2402 customStat["divs"][item["payment"]["currency"]] = payment 2403 2404 # count coupon's income: 2405 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2406 if item["payment"]["currency"] in customStat["coupons"].keys(): 2407 customStat["coupons"][item["payment"]["currency"]] += payment 2408 2409 else: 2410 customStat["coupons"][item["payment"]["currency"]] = payment 2411 2412 # count broker commissions: 2413 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2414 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2415 customStat["brokerCom"][item["payment"]["currency"]] += payment 2416 2417 else: 2418 customStat["brokerCom"][item["payment"]["currency"]] = payment 2419 2420 # count service commissions: 2421 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2422 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2423 customStat["serviceCom"][item["payment"]["currency"]] += payment 2424 2425 else: 2426 customStat["serviceCom"][item["payment"]["currency"]] = payment 2427 2428 # count margin commissions: 2429 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2430 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2431 customStat["marginCom"][item["payment"]["currency"]] += payment 2432 2433 else: 2434 customStat["marginCom"][item["payment"]["currency"]] = payment 2435 2436 # count withholding taxes: 2437 elif "_TAX" in item["operationType"]: 2438 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2439 customStat["allTaxes"][item["payment"]["currency"]] += payment 2440 2441 else: 2442 customStat["allTaxes"][item["payment"]["currency"]] = payment 2443 2444 else: 2445 continue 2446 2447 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2448 2449 # --- view "Actions" lines: 2450 info.extend([ 2451 "| Report sections | | | | |\n", 2452 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2453 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2454 "| | Buy: {:<22} | {:<28} | | |\n".format( 2455 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2456 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2457 ), 2458 "| | Sell: {:<21} | {:<28} | | |\n".format( 2459 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2460 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2461 ), 2462 ]) 2463 2464 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2465 for key in opsKeys: 2466 if key == "rub": 2467 continue 2468 2469 info.extend([ 2470 "| | | {:<28} | | |\n".format( 2471 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2472 ), 2473 "| | | {:<28} | | |\n".format( 2474 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2475 ), 2476 ]) 2477 2478 info.append(splitLine1) 2479 2480 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2481 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2482 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2483 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2484 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2485 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2486 ) 2487 2488 # --- view "Payments" lines: 2489 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2490 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2491 2492 for key in paymentsKeys: 2493 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2494 2495 info.append(splitLine1) 2496 2497 # --- view "Commissions and taxes" lines: 2498 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2499 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2500 2501 for key in comKeys: 2502 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2503 2504 info.append(splitLine1) 2505 2506 info.extend([ 2507 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2508 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2509 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2510 ]) 2511 2512 else: 2513 info.append("Broker returned no operations during this period\n") 2514 2515 # --- view "Operations" section: 2516 for item in ops: 2517 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2518 continue 2519 2520 else: 2521 self.figi = item["figi"] if item["figi"] else "" 2522 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2523 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2524 2525 # group of deals during one day: 2526 if nextDay and item["date"].split("T")[0] != nextDay: 2527 info.append(splitLine2) 2528 nextDay = "" 2529 2530 else: 2531 nextDay = item["date"].split("T")[0] # saving current day for splitting 2532 2533 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2534 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2535 self.figi if self.figi else "—", 2536 instrument["ticker"] if instrument else "—", 2537 instrument["type"] if instrument else "—", 2538 item["quantity"] if int(item["quantity"]) > 0 else "—", 2539 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2540 TKS_OPERATION_STATES[item["state"]], 2541 TKS_OPERATION_TYPES[item["operationType"]], 2542 )) 2543 2544 infoText = "".join(info) 2545 2546 if show: 2547 if self.moreDebug: 2548 uLogger.debug("Records about history of a client's operations successfully received") 2549 2550 uLogger.info(infoText) 2551 2552 if self.reportFile: 2553 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2554 fH.write(infoText) 2555 2556 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2557 2558 return ops, customStat 2559 2560 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2561 """ 2562 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2563 2564 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2565 Warning! Broker server used ISO UTC time by default. 2566 2567 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2568 Also, `historyFile` used to update history with `onlyMissing` parameter. 2569 2570 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2571 2572 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2573 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2574 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2575 `"hour"`, `"day"`. Default: `"hour"`. 2576 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2577 False by default. Warning! History appends only from last candle to current time 2578 with always update last candle! 2579 :param csvSep: separator if csv-file is used, `,` by default. 2580 :param show: if `True` then also prints Pandas DataFrame to the console. 2581 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2582 `["date", "time", "open", "high", "low", "close", "volume"]`. 2583 """ 2584 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2585 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2586 history = None # empty pandas object for history 2587 2588 if interval not in TKS_CANDLE_INTERVALS.keys(): 2589 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2590 raise Exception("Incorrect value") 2591 2592 if not (self.ticker or self.figi): 2593 uLogger.error("Ticker or FIGI must be defined!") 2594 raise Exception("Ticker or FIGI required") 2595 2596 if self.ticker and not self.figi: 2597 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2598 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2599 2600 if self.figi and not self.ticker: 2601 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2602 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2603 2604 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2605 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2606 if interval.lower() != "day": 2607 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2608 2609 delta = dtEnd - dtStart # current UTC time minus last time in file 2610 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2611 2612 # calculate history length in candles: 2613 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2614 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2615 length += 1 # to avoid fraction time 2616 2617 # calculate data blocks count: 2618 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2619 2620 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2621 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2622 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2623 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2624 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2625 2626 tempOld = None # pandas object for old history, if --only-missing key present 2627 lastTime = None # datetime object of last old candle in file 2628 2629 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2630 uLogger.debug("--only-missing key present, add only last missing candles...") 2631 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2632 2633 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2634 2635 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2636 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2637 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2638 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2639 2640 # get last datetime object from last string in file or minus 1 delta if file is empty: 2641 if len(tempOld) > 0: 2642 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2643 2644 else: 2645 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2646 2647 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2648 2649 responseJSONs = [] # raw history blocks of data 2650 2651 blockEnd = dtEnd 2652 for item in range(blocks): 2653 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2654 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2655 2656 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2657 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2658 )) 2659 2660 if blockStart == blockEnd: 2661 uLogger.debug("Skipped this zero-length block...") 2662 2663 else: 2664 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2665 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2666 self.body = str({ 2667 "figi": self.figi, 2668 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2669 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2670 "interval": TKS_CANDLE_INTERVALS[interval][0] 2671 }) 2672 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2673 2674 if "code" in responseJSON.keys(): 2675 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2676 2677 else: 2678 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2679 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2680 2681 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2682 2683 blockEnd = blockStart 2684 2685 printCount = len(responseJSONs) # candles to show in console 2686 if responseJSONs: 2687 tempHistory = pd.DataFrame( 2688 data={ 2689 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2690 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2691 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2692 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2693 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2694 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2695 "volume": [int(item["volume"]) for item in responseJSONs], 2696 }, 2697 index=range(len(responseJSONs)), 2698 columns=["date", "time", "open", "high", "low", "close", "volume"], 2699 ) 2700 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2701 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2702 2703 # append only newest candles to old history if --only-missing key present: 2704 if onlyMissing and tempOld is not None and lastTime is not None: 2705 index = 0 # find start index in tempHistory data: 2706 2707 for i, item in tempHistory.iterrows(): 2708 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2709 2710 if curTime == lastTime: 2711 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2712 index = i 2713 printCount = index + 1 2714 break 2715 2716 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2717 2718 else: 2719 history = tempHistory # if no `--only-missing` key then load full data from server 2720 2721 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2722 2723 if history is not None and not history.empty: 2724 if show: 2725 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2726 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2727 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2728 )) 2729 2730 else: 2731 uLogger.warning("Received an empty candles history!") 2732 2733 if self.historyFile is not None: 2734 if history is not None and not history.empty: 2735 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2736 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2737 2738 else: 2739 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2740 2741 else: 2742 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2743 2744 return history 2745 2746 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2747 """ 2748 Load candles history from csv-file and return Pandas DataFrame object. 2749 2750 See also: `History()` and `ShowHistoryChart()` methods. 2751 2752 :param filePath: path to csv-file to open. 2753 """ 2754 loadedHistory = None # init candles data object 2755 2756 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2757 2758 if os.path.exists(filePath): 2759 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2760 2761 tfStr = self.priceModel.FormattedDelta( 2762 self.priceModel.timeframe, 2763 "{days} days {hours}h {minutes}m {seconds}s", 2764 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2765 self.priceModel.timeframe, 2766 "{hours}h {minutes}m {seconds}s", 2767 ) 2768 2769 if loadedHistory is not None and not loadedHistory.empty: 2770 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2771 len(loadedHistory), 2772 tfStr, 2773 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2774 ) 2775 2776 else: 2777 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2778 2779 else: 2780 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2781 2782 return loadedHistory 2783 2784 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2785 """ 2786 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2787 2788 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2789 Default: `index.html` (both for interact and non-interact candlesticks chart). 2790 2791 See also: `History()` and `LoadHistory()` methods. 2792 2793 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2794 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2795 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2796 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2797 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2798 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2799 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2800 """ 2801 if isinstance(candles, str): 2802 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2803 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2804 2805 elif isinstance(candles, pd.DataFrame): 2806 self.priceModel.prices = candles # set candles chain from variable 2807 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2808 2809 if "datetime" not in candles.columns: 2810 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2811 2812 else: 2813 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2814 raise Exception("Incorrect value") 2815 2816 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2817 2818 if interact: 2819 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2820 2821 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2822 2823 else: 2824 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2825 2826 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2827 2828 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2829 2830 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2831 """ 2832 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2833 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2834 2835 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2836 2837 :param operation: string "Buy" or "Sell". 2838 :param lots: volume, integer count of lots >= 1. 2839 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2840 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2841 :param expDate: string "Undefined" by default or local date in future, 2842 it is a string with format `%Y-%m-%d %H:%M:%S`. 2843 :return: JSON with response from broker server. 2844 """ 2845 if self.accountId is None or not self.accountId: 2846 uLogger.error("Variable `accountId` must be defined for using this method!") 2847 raise Exception("Account ID required") 2848 2849 if operation is None or not operation or operation not in ("Buy", "Sell"): 2850 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2851 raise Exception("Incorrect value") 2852 2853 if lots is None or lots < 1: 2854 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2855 lots = 1 2856 2857 if tp is None or tp < 0: 2858 tp = 0 2859 2860 if sl is None or sl < 0: 2861 sl = 0 2862 2863 if expDate is None or not expDate: 2864 expDate = "Undefined" 2865 2866 if not (self.ticker or self.figi): 2867 uLogger.error("Ticker or FIGI must be defined!") 2868 raise Exception("Ticker or FIGI required") 2869 2870 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2871 self.ticker = instrument["ticker"] 2872 self.figi = instrument["figi"] 2873 2874 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2875 2876 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2877 self.body = str({ 2878 "figi": self.figi, 2879 "quantity": str(lots), 2880 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2881 "accountId": str(self.accountId), 2882 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2883 }) 2884 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2885 2886 if "orderId" in response.keys(): 2887 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2888 operation, response["orderId"], 2889 self.ticker, self.figi, lots, 2890 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2891 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2892 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2893 )) 2894 2895 if tp > 0: 2896 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2897 2898 if sl > 0: 2899 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2900 2901 else: 2902 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 2903 2904 return response 2905 2906 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2907 """ 2908 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2909 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2910 2911 See also: `Order()` and `Trade()` docstrings. 2912 2913 :param lots: volume, integer count of lots >= 1. 2914 :param tp: float > 0, take profit price of stop-order. 2915 :param sl: float > 0, stop loss price of stop-order. 2916 :param expDate: it's a local date in future. 2917 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2918 :return: JSON with response from broker server. 2919 """ 2920 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2921 2922 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2923 """ 2924 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2925 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2926 2927 See also: `Order()` and `Trade()` docstrings. 2928 2929 :param lots: volume, integer count of lots >= 1. 2930 :param tp: float > 0, take profit price of stop-order. 2931 :param sl: float > 0, stop loss price of stop-order. 2932 :param expDate: it's a local date in the future. 2933 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2934 :return: JSON with response from broker server. 2935 """ 2936 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2937 2938 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 2939 """ 2940 Close position of given instruments. 2941 2942 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 2943 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2944 This avoids unnecessary downloading data from the server. 2945 """ 2946 if instruments is None or not instruments: 2947 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 2948 raise Exception("Ticker or FIGI required") 2949 2950 if isinstance(instruments, str): 2951 instruments = [instruments] 2952 2953 uniqueInstruments = self.GetUniqueFIGIs(instruments) 2954 if uniqueInstruments: 2955 if portfolio is None or not portfolio: 2956 portfolio = self.Overview(show=False) 2957 2958 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2959 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 2960 2961 for self.figi in uniqueInstruments: 2962 if self.figi not in allOpened: 2963 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 2964 continue 2965 2966 # search open trade info about instrument by ticker: 2967 instrument = {} 2968 for iType in TKS_INSTRUMENTS: 2969 if instrument: 2970 break 2971 2972 for item in portfolio["stat"][iType]: 2973 if item["figi"] == self.figi: 2974 instrument = item 2975 break 2976 2977 if instrument: 2978 self.ticker = instrument["ticker"] 2979 self.figi = instrument["figi"] 2980 2981 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 2982 self.ticker, 2983 self.figi, 2984 int(instrument["volume"]), 2985 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 2986 )) 2987 2988 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 2989 2990 if tradeLots > 0: 2991 if instrument["blocked"] > 0: 2992 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 2993 instrument["blocked"], 2994 self.ticker, 2995 tradeLots, 2996 )) 2997 2998 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 2999 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3000 3001 else: 3002 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3003 3004 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3005 """ 3006 Close all positions of given instruments with defined type. 3007 3008 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3009 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3010 This avoids unnecessary downloading data from the server. 3011 """ 3012 if iType not in TKS_INSTRUMENTS: 3013 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3014 3015 else: 3016 if portfolio is None or not portfolio: 3017 portfolio = self.Overview(show=False) 3018 3019 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3020 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3021 3022 if tickers and portfolio: 3023 self.CloseTrades(tickers, portfolio) 3024 3025 else: 3026 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3027 3028 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3029 """ 3030 Universal method to create market or limit orders with all available parameters for current `accountId`. 3031 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3032 3033 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3034 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3035 3036 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3037 then broker immediately open market order as you can do simple --buy or --sell operations! 3038 3039 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3040 When current price will go up or down to target price value then broker opens a limit order. 3041 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3042 3043 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3044 3045 :param operation: string "Buy" or "Sell". 3046 :param orderType: string "Limit" or "Stop". 3047 :param lots: volume, integer count of lots >= 1. 3048 :param targetPrice: target price > 0. This is open trade price for limit order. 3049 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3050 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3051 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3052 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3053 Stop loss order always executed by market price. 3054 :param expDate: string "Undefined" by default or local date in future. 3055 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3056 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3057 A limit order has no expiration date, it lasts until the end of the trading day. 3058 :return: JSON with response from broker server. 3059 """ 3060 if self.accountId is None or not self.accountId: 3061 uLogger.error("Variable `accountId` must be defined for using this method!") 3062 raise Exception("Account ID required") 3063 3064 if operation is None or not operation or operation not in ("Buy", "Sell"): 3065 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3066 raise Exception("Incorrect value") 3067 3068 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3069 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3070 raise Exception("Incorrect value") 3071 3072 if lots is None or lots < 1: 3073 uLogger.error("You must define trade volume > 0: integer count of lots!") 3074 raise Exception("Incorrect value") 3075 3076 if targetPrice is None or targetPrice <= 0: 3077 uLogger.error("Target price for limit-order must be greater than 0!") 3078 raise Exception("Incorrect value") 3079 3080 if limitPrice is None or limitPrice <= 0: 3081 limitPrice = targetPrice 3082 3083 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3084 stopType = "Limit" 3085 3086 if expDate is None or not expDate: 3087 expDate = "Undefined" 3088 3089 if not (self.ticker or self.figi): 3090 uLogger.error("Tocker or FIGI must be defined!") 3091 raise Exception("Ticker or FIGI required") 3092 3093 response = {} 3094 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3095 self.ticker = instrument["ticker"] 3096 self.figi = instrument["figi"] 3097 3098 if orderType == "Limit": 3099 uLogger.debug( 3100 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3101 self.ticker, self.figi, 3102 operation, lots, targetPrice, instrument["currency"], 3103 )) 3104 3105 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3106 self.body = str({ 3107 "figi": self.figi, 3108 "quantity": str(lots), 3109 "price": FloatToNano(targetPrice), 3110 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3111 "accountId": str(self.accountId), 3112 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3113 }) 3114 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3115 3116 if "orderId" in response.keys(): 3117 uLogger.info( 3118 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3119 response["orderId"], 3120 self.ticker, self.figi, 3121 operation, lots, targetPrice, instrument["currency"], 3122 )) 3123 3124 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3125 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3126 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3127 targetPrice, instrument["currency"], 3128 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3129 )) 3130 3131 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3132 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3133 targetPrice, instrument["currency"], 3134 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3135 )) 3136 3137 else: 3138 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3139 3140 if orderType == "Stop": 3141 uLogger.debug( 3142 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3143 self.ticker, self.figi, 3144 operation, lots, 3145 targetPrice, instrument["currency"], 3146 limitPrice, instrument["currency"], 3147 stopType, expDate, 3148 )) 3149 3150 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3151 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3152 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3153 3154 body = { 3155 "figi": self.figi, 3156 "quantity": str(lots), 3157 "price": FloatToNano(limitPrice), 3158 "stopPrice": FloatToNano(targetPrice), 3159 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3160 "accountId": str(self.accountId), 3161 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3162 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3163 } 3164 3165 if expDateUTC: 3166 body["expireDate"] = expDateUTC 3167 3168 self.body = str(body) 3169 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3170 3171 if "stopOrderId" in response.keys(): 3172 uLogger.info( 3173 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3174 response["stopOrderId"], 3175 self.ticker, self.figi, 3176 operation, lots, 3177 targetPrice, instrument["currency"], 3178 limitPrice, instrument["currency"], 3179 TKS_STOP_ORDER_TYPES[stopOrderType], 3180 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3181 )) 3182 3183 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3184 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3185 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3186 targetPrice, instrument["currency"], 3187 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3188 )) 3189 3190 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3191 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3192 targetPrice, instrument["currency"], 3193 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3194 )) 3195 3196 else: 3197 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3198 3199 return response 3200 3201 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3202 """ 3203 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3204 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3205 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3206 See also: `Order()` docstring. 3207 3208 :param lots: volume, integer count of lots >= 1. 3209 :param targetPrice: target price > 0. This is open trade price for limit order. 3210 :return: JSON with response from broker server. 3211 """ 3212 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3213 3214 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3215 """ 3216 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3217 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3218 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3219 target price value then broker opens a limit order. See also: `Order()` docstring. 3220 3221 :param lots: volume, integer count of lots >= 1. 3222 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3223 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3224 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3225 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3226 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3227 :param expDate: string "Undefined" by default or local date in future. 3228 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3229 This date is converting to UTC format for server. 3230 :return: JSON with response from broker server. 3231 """ 3232 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3233 3234 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3235 """ 3236 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3237 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3238 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3239 See also: `Order()` docstring. 3240 3241 :param lots: volume, integer count of lots >= 1. 3242 :param targetPrice: target price > 0. This is open trade price for limit order. 3243 :return: JSON with response from broker server. 3244 """ 3245 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3246 3247 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3248 """ 3249 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3250 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3251 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3252 target price value then broker opens a limit order. See also: `Order()` docstring. 3253 3254 :param lots: volume, integer count of lots >= 1. 3255 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3256 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3257 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3258 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3259 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3260 :param expDate: string "Undefined" by default or local date in future. 3261 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3262 This date is converting to UTC format for server. 3263 :return: JSON with response from broker server. 3264 """ 3265 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3266 3267 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3268 """ 3269 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3270 3271 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3272 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3273 This avoids unnecessary downloading data from the server. 3274 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3275 """ 3276 if self.accountId is None or not self.accountId: 3277 uLogger.error("Variable `accountId` must be defined for using this method!") 3278 raise Exception("Account ID required") 3279 3280 if orderIDs: 3281 if allOrdersIDs is None: 3282 rawOrders = self.RequestPendingOrders() 3283 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3284 3285 if allStopOrdersIDs is None: 3286 rawStopOrders = self.RequestStopOrders() 3287 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3288 3289 for orderID in orderIDs: 3290 idInPendingOrders = orderID in allOrdersIDs 3291 idInStopOrders = orderID in allStopOrdersIDs 3292 3293 if not (idInPendingOrders or idInStopOrders): 3294 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3295 continue 3296 3297 else: 3298 if idInPendingOrders: 3299 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3300 3301 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3302 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3303 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3304 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3305 3306 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3307 if self.moreDebug: 3308 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3309 3310 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3311 3312 else: 3313 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3314 3315 elif idInStopOrders: 3316 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3317 3318 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3319 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3320 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3321 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3322 3323 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3324 if self.moreDebug: 3325 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3326 3327 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3328 3329 else: 3330 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3331 3332 else: 3333 continue 3334 3335 def CloseAllOrders(self) -> None: 3336 """ 3337 Gets a list of open pending and stop orders and cancel it all. 3338 """ 3339 rawOrders = self.RequestPendingOrders() 3340 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3341 lenOrders = len(allOrdersIDs) 3342 3343 rawStopOrders = self.RequestStopOrders() 3344 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3345 lenSOrders = len(allStopOrdersIDs) 3346 3347 if lenOrders > 0 or lenSOrders > 0: 3348 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3349 3350 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3351 3352 else: 3353 uLogger.info("Orders not found, nothing to cancel.") 3354 3355 def CloseAll(self, *args) -> None: 3356 """ 3357 Close all available (not blocked) opened trades and orders. 3358 3359 Also, you can select one or more keywords case-insensitive: 3360 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3361 3362 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3363 """ 3364 overview = self.Overview(show=False) # get all open trades info 3365 3366 if len(args) == 0: 3367 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3368 self.CloseAllOrders() # close all pending and stop orders 3369 3370 for iType in TKS_INSTRUMENTS: 3371 if iType != "Currencies": 3372 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3373 3374 else: 3375 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3376 lowerArgs = [x.lower() for x in args] 3377 3378 if "orders" in lowerArgs: 3379 self.CloseAllOrders() # close all pending and stop orders 3380 3381 for iType in TKS_INSTRUMENTS: 3382 if iType.lower() in lowerArgs and iType != "Currencies": 3383 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3384 3385 def CloseAllByTicker(self, instrument: str) -> None: 3386 """ 3387 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3388 3389 This method searches opened trade and orders of instrument throw all portfolio and then use 3390 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3391 3392 :param instrument: string with ticker. 3393 """ 3394 if instrument is None or not instrument: 3395 uLogger.error("Ticker name must be defined for using this method!") 3396 raise Exception("Ticker required") 3397 3398 overview = self.Overview(show=False) # get user portfolio with all open trades info 3399 3400 self.ticker = instrument # try to set instrument as ticker 3401 self.figi = "" 3402 3403 if self.IsInPortfolio(portfolio=overview): 3404 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3405 self.CloseTrades(instruments=[instrument], portfolio=overview) 3406 3407 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3408 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3409 3410 if limitAll and self.IsInLimitOrders(portfolio=overview): 3411 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3412 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3413 3414 if stopAll and self.IsInStopOrders(portfolio=overview): 3415 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3416 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3417 3418 def CloseAllByFIGI(self, instrument: str) -> None: 3419 """ 3420 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3421 3422 This method searches opened trade and orders of instrument throw all portfolio and then use 3423 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3424 3425 :param instrument: string with FIGI id. 3426 """ 3427 if instrument is None or not instrument: 3428 uLogger.error("FIGI id must be defined for using this method!") 3429 raise Exception("FIGI required") 3430 3431 overview = self.Overview(show=False) # get user portfolio with all open trades info 3432 3433 self.ticker = "" 3434 self.figi = instrument # try to set instrument as FIGI id 3435 3436 if self.IsInPortfolio(portfolio=overview): 3437 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3438 self.CloseTrades(instruments=[instrument], portfolio=overview) 3439 3440 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3441 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3442 3443 if limitAll and self.IsInLimitOrders(portfolio=overview): 3444 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3445 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3446 3447 if stopAll and self.IsInStopOrders(portfolio=overview): 3448 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3449 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3450 3451 @staticmethod 3452 def ParseOrderParameters(operation, **inputParameters): 3453 """ 3454 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3455 3456 :param operation: string "Buy" or "Sell". 3457 :param inputParameters: this is dict of strings that looks like this 3458 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3459 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3460 "prices" key: one or more prices to open limit-orders 3461 Counts of values in lots and prices lists must be equals! 3462 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3463 """ 3464 # TODO: update order grid work with api v2 3465 pass 3466 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3467 # 3468 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3469 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3470 # raise Exception("Incorrect value") 3471 # 3472 # if "l" in inputParameters.keys(): 3473 # inputParameters["lots"] = inputParameters.pop("l") 3474 # 3475 # if "p" in inputParameters.keys(): 3476 # inputParameters["prices"] = inputParameters.pop("p") 3477 # 3478 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3479 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3480 # raise Exception("Incorrect value") 3481 # 3482 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3483 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3484 # 3485 # if len(lots) != len(prices): 3486 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3487 # raise Exception("Incorrect value") 3488 # 3489 # uLogger.debug("Extracted parameters for orders:") 3490 # uLogger.debug("lots = {}".format(lots)) 3491 # uLogger.debug("prices = {}".format(prices)) 3492 # 3493 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3494 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3495 # uLogger.debug("Order parameters: {}".format(result)) 3496 # 3497 # return result 3498 3499 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3500 """ 3501 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3502 3503 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3504 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3505 """ 3506 result = False 3507 msg = "Instrument not defined!" 3508 3509 if portfolio is None or not portfolio: 3510 portfolio = self.Overview(show=False) 3511 3512 if self.ticker: 3513 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self.ticker)) 3514 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3515 3516 for iType in TKS_INSTRUMENTS: 3517 for instrument in portfolio["stat"][iType]: 3518 if instrument["ticker"] == self.ticker: 3519 result = True 3520 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3521 break 3522 3523 elif self.figi: 3524 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self.figi)) 3525 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3526 3527 for iType in TKS_INSTRUMENTS: 3528 for instrument in portfolio["stat"][iType]: 3529 if instrument["figi"] == self.figi: 3530 result = True 3531 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3532 break 3533 3534 else: 3535 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3536 3537 uLogger.debug(msg) 3538 3539 return result 3540 3541 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3542 """ 3543 Returns instrument from the user's portfolio if it presents there. 3544 Instrument must be defined by `ticker` (highly priority) or `figi`. 3545 3546 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3547 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3548 """ 3549 result = None 3550 msg = "Instrument not defined!" 3551 3552 if portfolio is None or not portfolio: 3553 portfolio = self.Overview(show=False) 3554 3555 if self.ticker: 3556 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker)) 3557 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3558 3559 for iType in TKS_INSTRUMENTS: 3560 for instrument in portfolio["stat"][iType]: 3561 if instrument["ticker"] == self.ticker: 3562 result = instrument 3563 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3564 break 3565 3566 elif self.figi: 3567 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3568 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3569 3570 for iType in TKS_INSTRUMENTS: 3571 for instrument in portfolio["stat"][iType]: 3572 if instrument["figi"] == self.figi: 3573 result = instrument 3574 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3575 break 3576 3577 else: 3578 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3579 3580 uLogger.debug(msg) 3581 3582 return result 3583 3584 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3585 """ 3586 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3587 3588 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3589 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3590 """ 3591 result = False 3592 msg = "Instrument not defined!" 3593 3594 if portfolio is None or not portfolio: 3595 portfolio = self.Overview(show=False) 3596 3597 if self.ticker: 3598 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self.ticker)) 3599 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self.ticker) 3600 3601 for instrument in portfolio["stat"]["orders"]: 3602 if instrument["ticker"] == self.ticker: 3603 result = True 3604 msg = "Instrument with ticker [{}] is present in limit orders list".format(self.ticker) 3605 break 3606 3607 elif self.figi: 3608 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self.figi)) 3609 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self.figi) 3610 3611 for instrument in portfolio["stat"]["orders"]: 3612 if instrument["figi"] == self.figi: 3613 result = True 3614 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self.figi) 3615 break 3616 3617 else: 3618 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3619 3620 uLogger.debug(msg) 3621 3622 return result 3623 3624 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3625 """ 3626 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3627 Instrument must be defined by `ticker` (highly priority) or `figi`. 3628 3629 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3630 :return: list with `orderID`s of limit orders. 3631 """ 3632 result = [] 3633 msg = "Instrument not defined!" 3634 3635 if portfolio is None or not portfolio: 3636 portfolio = self.Overview(show=False) 3637 3638 if self.ticker: 3639 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self.ticker)) 3640 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self.ticker) 3641 3642 for instrument in portfolio["stat"]["orders"]: 3643 if instrument["ticker"] == self.ticker: 3644 result.append(instrument["orderID"]) 3645 3646 if result: 3647 msg = "Instrument with ticker [{}] is present in limit orders list".format(self.ticker) 3648 3649 elif self.figi: 3650 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self.figi)) 3651 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self.figi) 3652 3653 for instrument in portfolio["stat"]["orders"]: 3654 if instrument["figi"] == self.figi: 3655 result.append(instrument["orderID"]) 3656 3657 if result: 3658 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self.figi) 3659 3660 else: 3661 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3662 3663 uLogger.debug(msg) 3664 3665 return result 3666 3667 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3668 """ 3669 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3670 3671 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3672 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3673 """ 3674 result = False 3675 msg = "Instrument not defined!" 3676 3677 if portfolio is None or not portfolio: 3678 portfolio = self.Overview(show=False) 3679 3680 if self.ticker: 3681 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self.ticker)) 3682 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self.ticker) 3683 3684 for instrument in portfolio["stat"]["stopOrders"]: 3685 if instrument["ticker"] == self.ticker: 3686 result = True 3687 msg = "Instrument with ticker [{}] is present in stop orders list".format(self.ticker) 3688 break 3689 3690 elif self.figi: 3691 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self.figi)) 3692 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self.figi) 3693 3694 for instrument in portfolio["stat"]["stopOrders"]: 3695 if instrument["figi"] == self.figi: 3696 result = True 3697 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self.figi) 3698 break 3699 3700 else: 3701 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3702 3703 uLogger.debug(msg) 3704 3705 return result 3706 3707 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3708 """ 3709 Returns list with all `orderID`s of opened stop orders for the instrument. 3710 Instrument must be defined by `ticker` (highly priority) or `figi`. 3711 3712 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3713 :return: list with `orderID`s of stop orders. 3714 """ 3715 result = [] 3716 msg = "Instrument not defined!" 3717 3718 if portfolio is None or not portfolio: 3719 portfolio = self.Overview(show=False) 3720 3721 if self.ticker: 3722 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self.ticker)) 3723 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self.ticker) 3724 3725 for instrument in portfolio["stat"]["stopOrders"]: 3726 if instrument["ticker"] == self.ticker: 3727 result.append(instrument["orderID"]) 3728 3729 if result: 3730 msg = "Instrument with ticker [{}] is present in stop orders list".format(self.ticker) 3731 3732 elif self.figi: 3733 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self.figi)) 3734 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self.figi) 3735 3736 for instrument in portfolio["stat"]["stopOrders"]: 3737 if instrument["figi"] == self.figi: 3738 result.append(instrument["orderID"]) 3739 3740 if result: 3741 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self.figi) 3742 3743 else: 3744 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3745 3746 uLogger.debug(msg) 3747 3748 return result 3749 3750 def RequestLimits(self) -> dict: 3751 """ 3752 Method for obtaining the available funds for withdrawal for current `accountId`. 3753 3754 See also: 3755 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3756 - `OverviewLimits()` method 3757 3758 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3759 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3760 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3761 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3762 """ 3763 if self.accountId is None or not self.accountId: 3764 uLogger.error("Variable `accountId` must be defined for using this method!") 3765 raise Exception("Account ID required") 3766 3767 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3768 3769 self.body = str({"accountId": self.accountId}) 3770 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3771 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3772 3773 if self.moreDebug: 3774 uLogger.debug("Records about available funds for withdrawal successfully received") 3775 3776 return rawLimits 3777 3778 def OverviewLimits(self, show: bool = False) -> dict: 3779 """ 3780 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3781 3782 See also: `RequestLimits()`. 3783 3784 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3785 :return: dict with raw parsed data from server and some calculated statistics about it. 3786 """ 3787 if self.accountId is None or not self.accountId: 3788 uLogger.error("Variable `accountId` must be defined for using this method!") 3789 raise Exception("Account ID required") 3790 3791 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3792 3793 view = { 3794 "rawLimits": rawLimits, 3795 "limits": { # parsed data for every currency: 3796 "money": { # this is an array of portfolio currency positions 3797 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3798 }, 3799 "blocked": { # this is an array of blocked currency 3800 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3801 }, 3802 "blockedGuarantee": { # this is locked money under collateral for futures 3803 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3804 }, 3805 }, 3806 } 3807 3808 # --- Prepare text table with limits in human-readable format: 3809 if show: 3810 info = [ 3811 "# Withdrawal limits\n\n", 3812 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3813 "* **Account ID:** [{}]\n".format(self.accountId), 3814 ] 3815 3816 if view["limits"]["money"]: 3817 info.extend([ 3818 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3819 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3820 ]) 3821 3822 else: 3823 info.append("\nNo withdrawal limits\n") 3824 3825 for curr in view["limits"]["money"].keys(): 3826 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3827 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3828 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3829 3830 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3831 "[{}]".format(curr), 3832 "{:.2f}".format(view["limits"]["money"][curr]), 3833 "{:.2f}".format(availableMoney), 3834 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3835 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3836 ) 3837 3838 if curr == "rub": 3839 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3840 3841 else: 3842 info.append(infoStr) 3843 3844 infoText = "".join(info) 3845 3846 uLogger.info(infoText) 3847 3848 if self.withdrawalLimitsFile: 3849 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3850 fH.write(infoText) 3851 3852 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3853 3854 return view 3855 3856 def RequestAccounts(self) -> dict: 3857 """ 3858 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3859 3860 See also: 3861 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3862 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3863 - `OverviewUserInfo()` method 3864 3865 :return: dict with raw data from server that contains accounts info. Example of dict: 3866 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3867 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3868 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3869 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3870 """ 3871 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3872 3873 self.body = str({}) 3874 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3875 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3876 3877 if self.moreDebug: 3878 uLogger.debug("Records about available accounts successfully received") 3879 3880 return rawAccounts 3881 3882 def RequestUserInfo(self) -> dict: 3883 """ 3884 Method for requesting common user's information. 3885 3886 See also: 3887 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3888 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3889 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3890 - `OverviewUserInfo()` method 3891 3892 :return: dict with raw data from server that contains user's information. Example of dict: 3893 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3894 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3895 """ 3896 uLogger.debug("Requesting common user's information. Wait, please...") 3897 3898 self.body = str({}) 3899 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3900 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3901 3902 if self.moreDebug: 3903 uLogger.debug("Records about current user successfully received") 3904 3905 return rawUserInfo 3906 3907 def RequestMarginStatus(self, accountId: str = None) -> dict: 3908 """ 3909 Method for requesting margin calculation for defined account ID. 3910 3911 See also: 3912 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3913 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3914 - `OverviewUserInfo()` method 3915 3916 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3917 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3918 Example of responses: 3919 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3920 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3921 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3922 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3923 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3924 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3925 """ 3926 if accountId is None or not accountId: 3927 if self.accountId is None or not self.accountId: 3928 uLogger.error("Variable `accountId` must be defined for using this method!") 3929 raise Exception("Account ID required") 3930 3931 else: 3932 accountId = self.accountId # use `self.accountId` (main ID) by default 3933 3934 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3935 3936 self.body = str({"accountId": accountId}) 3937 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3938 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3939 3940 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3941 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3942 rawMargin = {} 3943 3944 else: 3945 if self.moreDebug: 3946 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3947 3948 return rawMargin 3949 3950 def RequestTariffLimits(self) -> dict: 3951 """ 3952 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3953 3954 See also: 3955 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3956 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3957 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3958 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3959 - `OverviewUserInfo()` method 3960 3961 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3962 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3963 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3964 """ 3965 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3966 3967 self.body = str({}) 3968 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3969 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3970 3971 if self.moreDebug: 3972 uLogger.debug("Records with limits of current tariff successfully received") 3973 3974 return rawTariffLimits 3975 3976 def RequestBondCoupons(self, iJSON: dict) -> dict: 3977 """ 3978 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3979 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3980 All dates are in UTC timezone. 3981 3982 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3983 Documentation: 3984 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3985 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3986 3987 See also: `ExtendBondsData()`. 3988 3989 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3990 If raw iJSON is not data of bond then server returns an error [400] with message: 3991 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3992 :return: dictionary with bond payment calendar. Response example 3993 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3994 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3995 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3996 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3997 """ 3998 if iJSON["figi"] is None or not iJSON["figi"]: 3999 uLogger.error("FIGI must be defined for using this method!") 4000 raise Exception("FIGI required") 4001 4002 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4003 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4004 4005 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4006 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4007 self.figi, 4008 startDate, 4009 endDate, 4010 )) 4011 4012 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4013 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4014 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4015 4016 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4017 uLogger.warning("Instrument type is not bond!") 4018 4019 else: 4020 if self.moreDebug: 4021 uLogger.debug("Records about bond payment calendar successfully received") 4022 4023 return calendar 4024 4025 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4026 """ 4027 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4028 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4029 coupon yields, current yields and some statistics etc. 4030 4031 WARNING! This is too long operation if a lot of bonds requested from broker server. 4032 4033 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4034 4035 :param instruments: list of strings with tickers or FIGIs. 4036 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4037 for further used by data scientists or stock analytics. 4038 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4039 In XLSX-file and Pandas DataFrame fields mean: 4040 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4041 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4042 """ 4043 if instruments is None or not instruments: 4044 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4045 raise Exception("Ticker or FIGI required") 4046 4047 if isinstance(instruments, str): 4048 instruments = [instruments] 4049 4050 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4051 4052 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4053 4054 iCount = len(uniqueInstruments) 4055 tooLong = iCount >= 20 4056 if tooLong: 4057 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4058 4059 bonds = None 4060 for i, self.figi in enumerate(uniqueInstruments): 4061 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4062 4063 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4064 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4065 rawBond = self.SearchByFIGI(requestPrice=True) 4066 4067 # Widen raw data with UTC current time (iData["actualDateTime"]): 4068 actualDate = datetime.now(tzutc()) 4069 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4070 4071 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4072 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4073 4074 # Replace some values with human-readable: 4075 iData["nominalCurrency"] = iData["nominal"]["currency"] 4076 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4077 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4078 iData["aciCurrency"] = iData["aciValue"]["currency"] 4079 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4080 iData["issueSize"] = int(iData["issueSize"]) 4081 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4082 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4083 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4084 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4085 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4086 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4087 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4088 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4089 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4090 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4091 4092 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4093 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4094 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4095 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4096 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4097 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4098 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4099 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4100 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4101 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4102 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4103 4104 # Widen raw data with calendar data from `rawCalendar` values: 4105 calendarData = [] 4106 if "events" in iData["rawCalendar"].keys(): 4107 for item in iData["rawCalendar"]["events"]: 4108 calendarData.append({ 4109 "couponDate": item["couponDate"], 4110 "couponNumber": int(item["couponNumber"]), 4111 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4112 "payCurrency": item["payOneBond"]["currency"], 4113 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4114 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4115 "couponStartDate": item["couponStartDate"], 4116 "couponEndDate": item["couponEndDate"], 4117 "couponPeriod": item["couponPeriod"], 4118 }) 4119 4120 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4121 if "maturityDate" not in iData.keys(): 4122 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4123 4124 # Widen raw data with Coupon Rate. 4125 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4126 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4127 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4128 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4129 4130 # Widen raw data with Yield to Maturity (YTM) on current date. 4131 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4132 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4133 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4134 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4135 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4136 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4137 4138 iData["calendar"] = calendarData # adds calendar at the end 4139 4140 # Remove not used data: 4141 iData.pop("uid") 4142 iData.pop("positionUid") 4143 iData.pop("currentPrice") 4144 iData.pop("rawCalendar") 4145 4146 colNames = list(iData.keys()) 4147 if bonds is None: 4148 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4149 4150 else: 4151 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4152 4153 else: 4154 uLogger.warning("Instrument is not a bond!") 4155 4156 processed = round(100 * (i + 1) / iCount, 1) 4157 if tooLong and processed % 5 == 0: 4158 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4159 4160 else: 4161 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4162 4163 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4164 4165 # Saving bonds from Pandas DataFrame to XLSX sheet: 4166 if xlsx and self.bondsXLSXFile: 4167 with pd.ExcelWriter( 4168 path=self.bondsXLSXFile, 4169 date_format=TKS_DATE_FORMAT, 4170 datetime_format=TKS_DATE_TIME_FORMAT, 4171 mode="w", 4172 ) as writer: 4173 bonds.to_excel( 4174 writer, 4175 sheet_name="Extended bonds data", 4176 index=True, 4177 encoding="UTF-8", 4178 freeze_panes=(1, 1), 4179 ) # saving as XLSX-file with freeze first row and column as headers 4180 4181 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4182 4183 return bonds 4184 4185 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4186 """ 4187 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4188 4189 WARNING! This is too long operation if a lot of bonds requested from broker server. 4190 4191 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4192 4193 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4194 extended information about bonds: main info, current prices, bond payment calendar, 4195 coupon yields, current yields and some statistics etc. 4196 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4197 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4198 for further used by data scientists or stock analytics. 4199 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4200 """ 4201 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4202 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4203 4204 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4205 4206 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4207 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4208 calendar = None 4209 for bond in extBonds.iterrows(): 4210 for item in bond[1]["calendar"]: 4211 cData = { 4212 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4213 "couponDate": item["couponDate"], 4214 "figi": bond[1]["figi"], 4215 "ticker": bond[1]["ticker"], 4216 "name": bond[1]["name"], 4217 "couponNumber": item["couponNumber"], 4218 "payOneBond": item["payOneBond"], 4219 "payCurrency": item["payCurrency"], 4220 "couponType": item["couponType"], 4221 "couponPeriod": item["couponPeriod"], 4222 "fixDate": item["fixDate"], 4223 "couponStartDate": item["couponStartDate"], 4224 "couponEndDate": item["couponEndDate"], 4225 } 4226 4227 if calendar is None: 4228 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4229 4230 else: 4231 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4232 4233 if calendar is not None: 4234 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4235 4236 # Saving calendar from Pandas DataFrame to XLSX sheet: 4237 if xlsx: 4238 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4239 4240 with pd.ExcelWriter( 4241 path=xlsxCalendarFile, 4242 date_format=TKS_DATE_FORMAT, 4243 datetime_format=TKS_DATE_TIME_FORMAT, 4244 mode="w", 4245 ) as writer: 4246 humanReadable = calendar.copy(deep=True) 4247 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4248 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4249 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4250 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4251 humanReadable.columns = colNames # human-readable column names 4252 4253 humanReadable.to_excel( 4254 writer, 4255 sheet_name="Bond payments calendar", 4256 index=False, 4257 encoding="UTF-8", 4258 freeze_panes=(1, 2), 4259 ) # saving as XLSX-file with freeze first row and column as headers 4260 4261 del humanReadable # release df in memory 4262 4263 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4264 4265 return calendar 4266 4267 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4268 """ 4269 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4270 Also, creates Markdown file with calendar data, `calendar.md` by default. 4271 4272 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4273 4274 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4275 extended information about bonds: main info, current prices, bond payment calendar, 4276 coupon yields, current yields and some statistics etc. 4277 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4278 :param show: if `True` then also printing bonds payment calendar to the console, 4279 otherwise save to file `calendarFile` only. `False` by default. 4280 :return: multilines text in Markdown format with bonds payment calendar as a table. 4281 """ 4282 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4283 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4284 4285 infoText = "# Bond payments calendar\n\n" 4286 4287 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4288 4289 if not (calendar is None or calendar.empty): 4290 splitLine = "| | | | | | | | | |\n" 4291 4292 info = [ 4293 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4294 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4295 ] 4296 4297 newMonth = False 4298 notOneBond = calendar["figi"].nunique() > 1 4299 for i, bond in enumerate(calendar.iterrows()): 4300 if newMonth and notOneBond: 4301 info.append(splitLine) 4302 4303 info.append( 4304 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4305 " √" if bond[1]["paid"] else " —", 4306 bond[1]["couponDate"].split("T")[0], 4307 bond[1]["figi"], 4308 bond[1]["ticker"], 4309 bond[1]["couponNumber"], 4310 "{} {}".format( 4311 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4312 bond[1]["payCurrency"], 4313 ), 4314 bond[1]["couponType"], 4315 bond[1]["couponPeriod"], 4316 bond[1]["fixDate"].split("T")[0], 4317 ) 4318 ) 4319 4320 if i < len(calendar.values) - 1: 4321 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4322 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4323 newMonth = False if curDate.month == nextDate.month else True 4324 4325 else: 4326 newMonth = False 4327 4328 infoText += "".join(info) 4329 4330 if show: 4331 uLogger.info("{}".format(infoText)) 4332 4333 if self.calendarFile is not None: 4334 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4335 fH.write(infoText) 4336 4337 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4338 4339 else: 4340 infoText += "No data\n" 4341 4342 return infoText 4343 4344 def OverviewAccounts(self, show: bool = False) -> dict: 4345 """ 4346 Method for parsing and show simple table with all available user accounts. 4347 4348 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4349 4350 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4351 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4352 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4353 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4354 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4355 "closed": "—", "access": "Full access" }, ...}}` 4356 """ 4357 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4358 4359 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4360 accounts = { 4361 item["id"]: { 4362 "type": TKS_ACCOUNT_TYPES[item["type"]], 4363 "name": item["name"], 4364 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4365 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4366 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4367 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4368 } for item in rawAccounts["accounts"] 4369 } 4370 4371 # Raw and parsed data with some fields replaced in "stat" section: 4372 view = { 4373 "rawAccounts": rawAccounts, 4374 "stat": accounts, 4375 } 4376 4377 # --- Prepare simple text table with only accounts data in human-readable format: 4378 if show: 4379 info = [ 4380 "# User accounts\n\n", 4381 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4382 "| Account ID | Type | Status | Name |\n", 4383 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4384 ] 4385 4386 for account in view["stat"].keys(): 4387 info.extend([ 4388 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4389 account, 4390 view["stat"][account]["type"], 4391 view["stat"][account]["status"], 4392 view["stat"][account]["name"], 4393 ) 4394 ]) 4395 4396 infoText = "".join(info) 4397 4398 uLogger.info(infoText) 4399 4400 if self.userAccountsFile: 4401 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4402 fH.write(infoText) 4403 4404 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4405 4406 return view 4407 4408 def OverviewUserInfo(self, show: bool = False) -> dict: 4409 """ 4410 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4411 4412 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4413 4414 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4415 :return: dict with raw parsed data from server and some calculated statistics about it. 4416 """ 4417 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4418 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4419 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4420 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4421 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4422 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4423 4424 # This is dict with parsed common user data: 4425 userInfo = { 4426 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4427 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4428 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4429 "tariff": rawUserInfo["tariff"], 4430 } 4431 4432 # This is an array of dict with parsed margin statuses for every account IDs: 4433 margins = {} 4434 for accountId in accounts.keys(): 4435 if rawMargins[accountId]: 4436 margins[accountId] = { 4437 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4438 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4439 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4440 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4441 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4442 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4443 } 4444 4445 else: 4446 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4447 4448 unary = {} # unary-connection limits 4449 for item in rawTariffLimits["unaryLimits"]: 4450 if item["limitPerMinute"] in unary.keys(): 4451 unary[item["limitPerMinute"]].extend(item["methods"]) 4452 4453 else: 4454 unary[item["limitPerMinute"]] = item["methods"] 4455 4456 stream = {} # stream-connection limits 4457 for item in rawTariffLimits["streamLimits"]: 4458 if item["limit"] in stream.keys(): 4459 stream[item["limit"]].extend(item["streams"]) 4460 4461 else: 4462 stream[item["limit"]] = item["streams"] 4463 4464 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4465 limits = { 4466 "unary": unary, 4467 "stream": stream, 4468 } 4469 4470 # Raw and parsed data as an output result: 4471 view = { 4472 "rawUserInfo": rawUserInfo, 4473 "rawAccounts": rawAccounts, 4474 "rawMargins": rawMargins, 4475 "rawTariffLimits": rawTariffLimits, 4476 "stat": { 4477 "userInfo": userInfo, 4478 "accounts": accounts, 4479 "margins": margins, 4480 "limits": limits, 4481 }, 4482 } 4483 4484 # --- Prepare text table with user information in human-readable format: 4485 if show: 4486 info = [ 4487 "# Full user information\n\n", 4488 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4489 "## Common information\n\n", 4490 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4491 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4492 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4493 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4494 "\n## User accounts\n\n", 4495 ] 4496 4497 for account in view["stat"]["accounts"].keys(): 4498 info.extend([ 4499 "### ID: [{}]\n\n".format(account), 4500 "| Parameters | Values |\n", 4501 "|----------------------|--------------------------------------------------------------|\n", 4502 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4503 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4504 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4505 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4506 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4507 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4508 ]) 4509 4510 if margins[account]: 4511 info.extend([ 4512 "| Margin status: | Enabled |\n", 4513 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4514 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4515 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4516 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4517 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4518 ]) 4519 4520 else: 4521 info.append("| Margin status: | Disabled |\n\n") 4522 4523 info.extend([ 4524 "\n## Current user tariff limits\n", 4525 "\nSee also:\n", 4526 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4527 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4528 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4529 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4530 "\n### Unary limits\n", 4531 ]) 4532 4533 if unary: 4534 for key, values in sorted(unary.items()): 4535 info.append("\n* Max requests per minute: {}\n".format(key)) 4536 4537 for value in values: 4538 info.append(" - {}\n".format(value)) 4539 4540 else: 4541 info.append("\nNot available\n") 4542 4543 info.append("\n### Stream limits\n") 4544 4545 if stream: 4546 for key, values in sorted(stream.items()): 4547 info.append("\n* Max stream connections: {}\n".format(key)) 4548 4549 for value in values: 4550 info.append(" - {}\n".format(value)) 4551 4552 else: 4553 info.append("\nNot available\n") 4554 4555 infoText = "".join(info) 4556 4557 uLogger.info(infoText) 4558 4559 if self.userInfoFile: 4560 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4561 fH.write(infoText) 4562 4563 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4564 4565 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
84 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 85 """ 86 Main class init. 87 88 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 89 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 90 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 91 :param useCache: use default cache file with raw data to use instead of `iList`. 92 True by default. Cache is auto-update if new day has come. 93 If you don't want to use cache and always updates raw data then set `useCache=False`. 94 :param defaultCache: path to default cache file. `dump.json` by default. 95 """ 96 if token is None or not token: 97 try: 98 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 99 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 100 101 except KeyError: 102 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 103 raise Exception("Token required") 104 105 else: 106 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 107 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 108 109 if accountId is None or not accountId: 110 try: 111 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 112 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 113 114 except KeyError: 115 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 116 117 else: 118 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 119 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 120 121 self.version = __version__ # duplicate here used TKSBrokerAPI main version 122 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 123 124 Latest version: https://pypi.org/project/tksbrokerapi/ 125 """ 126 127 self.aliases = TKS_TICKER_ALIASES 128 """Some aliases instead official tickers. 129 130 See also: `TKSEnums.TKS_TICKER_ALIASES` 131 """ 132 133 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 134 135 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 136 137 self.ticker = "" 138 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 139 140 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 141 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 142 143 See also: `SearchByTicker()`, `SearchInstruments()`. 144 """ 145 146 self.figi = "" 147 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 148 149 See also: `SearchByFIGI()`, `SearchInstruments()`. 150 """ 151 152 self.depth = 1 153 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 154 155 See also: `GetCurrentPrices()`. 156 """ 157 158 self.server = r"https://invest-public-api.tinkoff.ru/rest" 159 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 160 161 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 162 """ 163 164 uLogger.debug("Broker API server: {}".format(self.server)) 165 166 self.timeout = 15 167 """Server operations timeout in seconds. Default: `15`. 168 169 See also: `SendAPIRequest()`. 170 """ 171 172 self.headers = { 173 "Content-Type": "application/json", 174 "accept": "application/json", 175 "Authorization": "Bearer {}".format(self.token), 176 "x-app-name": "Tim55667757.TKSBrokerAPI", 177 } 178 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 179 180 See also: `SendAPIRequest()`. 181 """ 182 183 self.body = None 184 """Request body which send to broker server. Default: `None`. 185 186 See also: `SendAPIRequest()`. 187 """ 188 189 self.moreDebug = False 190 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 191 192 self.historyFile = None 193 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 194 195 See also: `History()`. 196 """ 197 198 self.htmlHistoryFile = "index.html" 199 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 200 201 See also: `ShowHistoryChart()`. 202 """ 203 204 self.instrumentsFile = "instruments.md" 205 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 206 207 See also: `ShowInstrumentsInfo()`. 208 """ 209 210 self.searchResultsFile = "search-results.md" 211 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 212 213 See also: `SearchInstruments()`. 214 """ 215 216 self.pricesFile = "prices.md" 217 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 218 219 See also: `GetListOfPrices()`. 220 """ 221 222 self.infoFile = "info.md" 223 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 224 225 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 226 """ 227 228 self.bondsXLSXFile = "ext-bonds.xlsx" 229 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 230 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 231 232 See also: `ExtendBondsData()`. 233 """ 234 235 self.calendarFile = "calendar.md" 236 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 237 238 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 239 240 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 241 """ 242 243 self.overviewFile = "overview.md" 244 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 245 246 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 247 """ 248 249 self.overviewDigestFile = "overview-digest.md" 250 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 251 252 See also: `Overview()` with parameter `details="digest"`. 253 """ 254 255 self.overviewPositionsFile = "overview-positions.md" 256 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 257 258 See also: `Overview()` with parameter `details="positions"`. 259 """ 260 261 self.overviewOrdersFile = "overview-orders.md" 262 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 263 264 See also: `Overview()` with parameter `details="orders"`. 265 """ 266 267 self.overviewAnalyticsFile = "overview-analytics.md" 268 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 269 270 See also: `Overview()` with parameter `details="analytics"`. 271 """ 272 273 self.overviewBondsCalendarFile = "overview-calendar.md" 274 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 275 276 See also: `Overview()` with parameter `details="calendar"`. 277 """ 278 279 self.reportFile = "deals.md" 280 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 281 282 See also: `Deals()`. 283 """ 284 285 self.withdrawalLimitsFile = "limits.md" 286 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 287 288 See also: `OverviewLimits()` and `RequestLimits()`. 289 """ 290 291 self.userInfoFile = "user-info.md" 292 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 293 294 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 295 """ 296 297 self.userAccountsFile = "accounts.md" 298 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 299 300 See also: `OverviewAccounts()`, `RequestAccounts()`. 301 """ 302 303 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 304 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 305 306 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 307 308 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 309 """ 310 311 self.iList = None # init iList for raw instruments data 312 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 313 314 See also: `Listing()`, `DumpInstruments()`. 315 """ 316 317 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 318 if useCache: 319 if os.path.exists(self.iListDumpFile): 320 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 321 curTime = datetime.now(tzutc()) 322 323 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 324 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 325 326 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 327 328 else: 329 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 330 331 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 332 os.path.abspath(self.iListDumpFile), 333 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 334 )) 335 336 else: 337 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 338 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 339 340 else: 341 self.iList = self.Listing() # request new raw instruments data from broker server 342 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 343 344 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 345 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 346 347 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 348 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
String with ticker, e.g. GOOGL. Tickers may be upper case only.
Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc.
More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.
See also: SearchByFIGI(), SearchInstruments().
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.
See also: Overview() with parameter details="calendar".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
364 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 365 """ 366 Send GET or POST request to broker server and receive JSON object. 367 368 self.header: must be defining with dictionary of headers. 369 self.body: if define then used as request body. None by default. 370 self.timeout: global request timeout, 15 seconds by default. 371 :param url: url with REST request. 372 :param reqType: send "GET" or "POST" request. "GET" by default. 373 :param retry: how many times retry after first request if an 5xx server errors occurred. 374 :param pause: sleep time in seconds between retries. 375 :return: response JSON (dictionary) from broker. 376 """ 377 if reqType.upper() not in ("GET", "POST"): 378 uLogger.error("You can define request type: `GET` or `POST`!") 379 raise Exception("Incorrect value") 380 381 if self.moreDebug: 382 uLogger.debug("Request parameters:") 383 uLogger.debug(" - REST API URL: {}".format(url)) 384 uLogger.debug(" - request type: {}".format(reqType)) 385 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 386 uLogger.debug(" - body:\n{}".format(self.body)) 387 388 # fast hack to avoid all operations with some tickers/FIGI 389 responseJSON = {} 390 oK = True 391 for item in self.exclude: 392 if item in url: 393 if self.moreDebug: 394 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 395 396 oK = False 397 break 398 399 if oK: 400 counter = 0 401 response = None 402 errMsg = "" 403 404 while not response and counter <= retry: 405 if reqType == "GET": 406 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 407 408 if reqType == "POST": 409 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 410 411 if self.moreDebug: 412 uLogger.debug("Response:") 413 uLogger.debug(" - status code: {}".format(response.status_code)) 414 uLogger.debug(" - reason: {}".format(response.reason)) 415 uLogger.debug(" - body length: {}".format(len(response.text))) 416 uLogger.debug(" - headers:\n{}".format(response.headers)) 417 418 # Server returns some headers: 419 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 420 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 421 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 422 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 423 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 424 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 425 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 426 sleep(rateLimitWait) 427 428 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 429 if 400 <= response.status_code < 500: 430 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 431 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 432 433 if "code" in response.text and "message" in response.text: 434 msgDict = self._ParseJSON(rawData=response.text) 435 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 436 437 counter = retry + 1 # do not retry for 4xx errors 438 439 if 500 <= response.status_code < 600: 440 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 441 uLogger.debug(" - not oK, {}".format(errMsg)) 442 443 if "code" in response.text and "message" in response.text: 444 errMsgDict = self._ParseJSON(rawData=response.text) 445 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 446 447 counter += 1 448 449 if counter <= retry: 450 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 451 sleep(pause) 452 453 responseJSON = self._ParseJSON(rawData=response.text) 454 455 if errMsg: 456 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 457 uLogger.error(" - not oK, {}".format(errMsg)) 458 459 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
492 def Listing(self) -> dict: 493 """ 494 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 495 496 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 497 """ 498 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 499 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 500 501 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 502 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 503 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 504 505 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 506 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 507 poolUpdater.close() 508 509 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 510 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 511 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 512 513 # calculate minimum price increment (step) for all instruments and set up instrument's type: 514 for iType in iList.keys(): 515 for ticker in iList[iType]: 516 iList[iType][ticker]["type"] = iType 517 518 if "minPriceIncrement" in iList[iType][ticker].keys(): 519 iList[iType][ticker]["step"] = NanoToFloat( 520 iList[iType][ticker]["minPriceIncrement"]["units"], 521 iList[iType][ticker]["minPriceIncrement"]["nano"], 522 ) 523 524 else: 525 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 526 527 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
529 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 530 """ 531 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 532 533 See also: `DumpInstruments()`, `Listing()`. 534 535 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 536 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 537 """ 538 if self.iListDumpFile is None or not self.iListDumpFile: 539 uLogger.error("Output name of dump file must be defined!") 540 raise Exception("Filename required") 541 542 if not self.iList or forceUpdate: 543 self.iList = self.Listing() 544 545 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 546 547 # Save as XLSX with separated sheets for every type of instruments: 548 with pd.ExcelWriter( 549 path=xlsxDumpFile, 550 date_format=TKS_DATE_FORMAT, 551 datetime_format=TKS_DATE_TIME_FORMAT, 552 mode="w", 553 ) as writer: 554 for iType in TKS_INSTRUMENTS: 555 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 556 df = df[sorted(df)] # sorted by column names 557 df = df.applymap( 558 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 559 na_action="ignore", 560 ) # converting numbers from nano-type to float in every cell 561 df.to_excel( 562 writer, 563 sheet_name=iType, 564 encoding="UTF-8", 565 freeze_panes=(1, 1), 566 ) # saving as XLSX-file with freeze first row and column as headers 567 568 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
570 def DumpInstruments(self, forceUpdate: bool = True) -> str: 571 """ 572 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 573 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 574 575 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 576 577 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 578 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 579 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 580 """ 581 if self.iListDumpFile is None or not self.iListDumpFile: 582 uLogger.error("Output name of dump file must be defined!") 583 raise Exception("Filename required") 584 585 if not self.iList or forceUpdate: 586 self.iList = self.Listing() 587 588 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 589 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 590 fH.write(jsonDump) 591 592 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 593 594 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
596 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 597 """ 598 Show information about one instrument defined by json data and prints it in Markdown format. 599 600 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 601 602 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 603 :param show: if `True` then also printing information about instrument and its current price. 604 :return: multilines text in Markdown format with information about one instrument. 605 """ 606 splitLine = "| | |\n" 607 infoText = "" 608 609 if iJSON is not None and iJSON and isinstance(iJSON, dict): 610 info = [ 611 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 612 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 613 "| Parameters | Values |\n", 614 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 615 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 616 "| Full name: | {:<54} |\n".format(iJSON["name"]), 617 ] 618 619 if "sector" in iJSON.keys() and iJSON["sector"]: 620 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 621 622 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 623 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 624 625 info.extend([ 626 splitLine, 627 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 628 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 629 ]) 630 631 if "isin" in iJSON.keys() and iJSON["isin"]: 632 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 633 634 if "classCode" in iJSON.keys(): 635 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 636 637 info.extend([ 638 splitLine, 639 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 640 splitLine, 641 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 642 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 643 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 644 ]) 645 646 if iJSON["figi"]: 647 self.figi = iJSON["figi"] 648 iJSON = iJSON | self.RequestTradingStatus() 649 650 info.extend([ 651 splitLine, 652 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 653 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 654 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 655 ]) 656 657 info.append(splitLine) 658 659 if "type" in iJSON.keys() and iJSON["type"]: 660 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 661 662 if "shareType" in iJSON.keys() and iJSON["shareType"]: 663 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 664 665 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 666 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 667 668 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 669 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 670 671 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 672 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 673 674 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 675 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 676 677 if "focusType" in iJSON.keys() and iJSON["focusType"]: 678 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 679 680 if "assetType" in iJSON.keys() and iJSON["assetType"]: 681 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 682 683 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 684 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 685 686 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 687 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 688 689 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 690 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 691 692 if "currency" in iJSON.keys(): 693 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 694 695 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 696 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 697 698 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 699 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 700 701 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 702 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 703 704 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 705 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 706 707 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 708 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 709 710 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 711 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 712 713 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 714 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 715 716 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 717 info.append("| Perpetual bond: | Yes |\n") 718 719 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 720 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 721 722 iExt = None 723 if iJSON["type"] == "Bonds": 724 info.extend([ 725 splitLine, 726 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 727 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 728 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 729 iJSON["nominal"]["currency"], 730 )), 731 ]) 732 733 if "floatingCouponFlag" in iJSON.keys(): 734 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 735 736 if "amortizationFlag" in iJSON.keys(): 737 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 738 739 info.append(splitLine) 740 741 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 742 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 743 744 if iJSON["figi"]: 745 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 746 747 info.extend([ 748 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 749 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 750 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 751 ]) 752 753 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 754 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 755 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 756 iJSON["aciValue"]["currency"] 757 ))) 758 759 if "currentPrice" in iJSON.keys(): 760 info.append(splitLine) 761 762 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 763 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 764 765 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 766 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 767 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 768 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 769 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 770 771 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 772 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 773 774 info.extend([ 775 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 776 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 777 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 778 )), 779 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 780 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 781 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 782 )), 783 "| Changes between last deal price and last close | {:<54} |\n".format( 784 "{:.2f}%{}".format( 785 iJSON["currentPrice"]["changes"], 786 " ({}{:.2f} {})".format( 787 "+" if bondChangesDelta > 0 else "", 788 bondChangesDelta, 789 aciCurrency 790 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 791 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 792 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 793 currency 794 ), 795 ) 796 ), 797 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 798 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 799 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 800 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 801 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 802 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 803 )), 804 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 805 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 806 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 807 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 808 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 809 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 810 )), 811 ]) 812 813 if "lot" in iJSON.keys(): 814 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 815 816 if "step" in iJSON.keys() and iJSON["step"] != 0: 817 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 818 819 # Add bond payment calendar: 820 if iJSON["type"] == "Bonds": 821 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 822 info.extend(["\n", strCalendar]) 823 824 infoText += "".join(info) 825 826 if show: 827 uLogger.info("{}".format(infoText)) 828 829 else: 830 uLogger.debug("{}".format(infoText)) 831 832 if self.infoFile is not None: 833 with open(self.infoFile, "w", encoding="UTF-8") as fH: 834 fH.write(infoText) 835 836 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 837 838 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self.ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
840 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 841 """ 842 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 843 844 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 845 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 846 :return: JSON formatted data with information about instrument. 847 """ 848 tickerJSON = {} 849 if self.moreDebug: 850 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 851 852 if not self.ticker: 853 uLogger.warning("self.ticker variable is not be empty!") 854 855 else: 856 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 857 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 858 raise Exception("Instrument not allowed") 859 860 if not self.iList: 861 self.iList = self.Listing() 862 863 if self.ticker in self.iList["Shares"].keys(): 864 tickerJSON = self.iList["Shares"][self.ticker] 865 if self.moreDebug: 866 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 867 868 elif self.ticker in self.iList["Currencies"].keys(): 869 tickerJSON = self.iList["Currencies"][self.ticker] 870 if self.moreDebug: 871 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 872 873 elif self.ticker in self.iList["Bonds"].keys(): 874 tickerJSON = self.iList["Bonds"][self.ticker] 875 if self.moreDebug: 876 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 877 878 elif self.ticker in self.iList["Etfs"].keys(): 879 tickerJSON = self.iList["Etfs"][self.ticker] 880 if self.moreDebug: 881 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 882 883 elif self.ticker in self.iList["Futures"].keys(): 884 tickerJSON = self.iList["Futures"][self.ticker] 885 if self.moreDebug: 886 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 887 888 if tickerJSON: 889 self.figi = tickerJSON["figi"] 890 891 if requestPrice: 892 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 893 894 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 895 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 896 897 else: 898 tickerJSON["currentPrice"]["changes"] = 0 899 900 if show: 901 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 902 903 else: 904 if show: 905 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 906 907 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
909 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 910 """ 911 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 912 913 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 914 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 915 :return: JSON formatted data with information about instrument. 916 """ 917 figiJSON = {} 918 if self.moreDebug: 919 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 920 921 if not self.figi: 922 uLogger.warning("self.figi variable is not be empty!") 923 924 else: 925 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 926 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 927 raise Exception("Instrument not allowed") 928 929 if not self.iList: 930 self.iList = self.Listing() 931 932 for item in self.iList["Shares"].keys(): 933 if self.figi == self.iList["Shares"][item]["figi"]: 934 figiJSON = self.iList["Shares"][item] 935 936 if self.moreDebug: 937 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 938 939 break 940 941 if not figiJSON: 942 for item in self.iList["Currencies"].keys(): 943 if self.figi == self.iList["Currencies"][item]["figi"]: 944 figiJSON = self.iList["Currencies"][item] 945 946 if self.moreDebug: 947 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 948 949 break 950 951 if not figiJSON: 952 for item in self.iList["Bonds"].keys(): 953 if self.figi == self.iList["Bonds"][item]["figi"]: 954 figiJSON = self.iList["Bonds"][item] 955 956 if self.moreDebug: 957 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 958 959 break 960 961 if not figiJSON: 962 for item in self.iList["Etfs"].keys(): 963 if self.figi == self.iList["Etfs"][item]["figi"]: 964 figiJSON = self.iList["Etfs"][item] 965 966 if self.moreDebug: 967 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 968 969 break 970 971 if not figiJSON: 972 for item in self.iList["Futures"].keys(): 973 if self.figi == self.iList["Futures"][item]["figi"]: 974 figiJSON = self.iList["Futures"][item] 975 976 if self.moreDebug: 977 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 978 979 break 980 981 if figiJSON: 982 self.figi = figiJSON["figi"] 983 self.ticker = figiJSON["ticker"] 984 985 if requestPrice: 986 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 987 988 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 989 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 990 991 else: 992 figiJSON["currentPrice"]["changes"] = 0 993 994 if show: 995 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 996 997 else: 998 if show: 999 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1000 1001 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1003 def GetCurrentPrices(self, show: bool = True) -> dict: 1004 """ 1005 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1006 `{"buy": [{"price": 1243.8, "quantity": 193}, 1007 {"price": 1244.0, "quantity": 168}, 1008 {"price": 1244.8, "quantity": 5}, 1009 {"price": 1245.0, "quantity": 61}, 1010 {"price": 1245.4, "quantity": 60}], 1011 "sell": [{"price": 1243.6, "quantity": 8}, 1012 {"price": 1242.6, "quantity": 10}, 1013 {"price": 1242.4, "quantity": 18}, 1014 {"price": 1242.2, "quantity": 50}, 1015 {"price": 1242.0, "quantity": 113}], 1016 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1017 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1018 - sell: list of dicts with Buyers prices, 1019 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1020 - quantity: volume value by current price in lots, 1021 - limitUp: current trade session limit price, maximum, 1022 - limitDown: current trade session limit price, minimum, 1023 - lastPrice: last deal price of the instrument, 1024 - closePrice: previous trade session close price of the instrument. 1025 1026 See also: `SearchByTicker()` and `SearchByFIGI()`. 1027 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1028 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1029 1030 :param show: if `True` then print DOM to log and console. 1031 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1032 If an error occurred then returns an empty record: 1033 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1034 """ 1035 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1036 1037 if self.depth < 1: 1038 uLogger.error("Depth of Market (DOM) must be >=1!") 1039 raise Exception("Incorrect value") 1040 1041 if not (self.ticker or self.figi): 1042 uLogger.error("self.ticker or self.figi variables must be defined!") 1043 raise Exception("Ticker or FIGI required") 1044 1045 if self.ticker and not self.figi: 1046 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1047 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1048 1049 if not self.ticker and self.figi: 1050 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1051 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1052 1053 if not self.figi: 1054 uLogger.error("FIGI is not defined!") 1055 raise Exception("Ticker or FIGI required") 1056 1057 else: 1058 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1059 1060 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1061 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1062 self.body = str({"figi": self.figi, "depth": self.depth}) 1063 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1064 1065 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1066 # list of dicts with sellers orders: 1067 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1068 1069 # list of dicts with buyers orders: 1070 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1071 1072 # max price of instrument at this time: 1073 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1074 1075 # min price of instrument at this time: 1076 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1077 1078 # last price of deal with instrument: 1079 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1080 1081 # last close price of instrument: 1082 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1083 1084 else: 1085 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1086 uLogger.debug("Server response: {}".format(pricesResponse)) 1087 1088 if show: 1089 if prices["buy"] or prices["sell"]: 1090 info = [ 1091 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1092 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1093 self.ticker, 1094 self.figi, 1095 self.depth, 1096 ), 1097 "-" * 60, "\n", 1098 " Orders of Buyers | Orders of Sellers\n", 1099 "-" * 60, "\n", 1100 " Sell prices (volumes) | Buy prices (volumes)\n", 1101 "-" * 60, "\n", 1102 ] 1103 1104 if not prices["buy"]: 1105 info.append(" | No orders!\n") 1106 sumBuy = 0 1107 1108 else: 1109 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1110 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1111 for item in maxMinSorted: 1112 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1113 1114 if not prices["sell"]: 1115 info.append("No orders! |\n") 1116 sumSell = 0 1117 1118 else: 1119 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1120 for item in prices["sell"]: 1121 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1122 1123 info.extend([ 1124 "-" * 60, "\n", 1125 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1126 "-" * 60, "\n", 1127 ]) 1128 1129 infoText = "".join(info) 1130 1131 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1132 1133 else: 1134 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1135 1136 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1138 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1139 """ 1140 This method get and show information about all available broker instruments for current user account. 1141 If `instrumentsFile` string is not empty then also save information to this file. 1142 1143 :param show: if `True` then print results to console, if `False` — print only to file. 1144 :return: multi-lines string with all available broker instruments 1145 """ 1146 if not self.iList: 1147 self.iList = self.Listing() 1148 1149 info = [ 1150 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1151 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1152 ] 1153 1154 # add instruments count by type: 1155 for iType in self.iList.keys(): 1156 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1157 1158 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1159 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1160 1161 # generating info tables with all instruments by type: 1162 for iType in self.iList.keys(): 1163 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1164 1165 for instrument in self.iList[iType].keys(): 1166 iName = self.iList[iType][instrument]["name"] # instrument's name 1167 if len(iName) > 57: 1168 iName = "{}...".format(iName[:54]) # right trim for a long string 1169 1170 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1171 self.iList[iType][instrument]["ticker"], 1172 iName, 1173 self.iList[iType][instrument]["figi"], 1174 self.iList[iType][instrument]["currency"], 1175 self.iList[iType][instrument]["lot"], 1176 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1177 )) 1178 1179 infoText = "".join(info) 1180 1181 if show: 1182 uLogger.info(infoText) 1183 1184 if self.instrumentsFile: 1185 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1186 fH.write(infoText) 1187 1188 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1189 1190 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse— print only to file.
Returns
multi-lines string with all available broker instruments
1192 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1193 """ 1194 This method search and show information about instruments by part of its ticker, FIGI or name. 1195 If `searchResultsFile` string is not empty then also save information to this file. 1196 1197 :param pattern: string with part of ticker, FIGI or instrument's name. 1198 :param show: if `True` then print results to console, if `False` — return list of result only. 1199 :return: list of dictionaries with all found instruments. 1200 """ 1201 if not self.iList: 1202 self.iList = self.Listing() 1203 1204 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1205 compiledPattern = re.compile(pattern, re.IGNORECASE) 1206 1207 for iType in self.iList: 1208 for instrument in self.iList[iType].values(): 1209 searchResult = compiledPattern.search(" ".join( 1210 [instrument["ticker"], instrument["figi"], instrument["name"]] 1211 )) 1212 1213 if searchResult: 1214 searchResults[iType][instrument["ticker"]] = instrument 1215 1216 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1217 info = [ 1218 "# Search results\n\n", 1219 "* **Search pattern:** [{}]\n".format(pattern), 1220 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1221 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1222 ] 1223 infoShort = info[:] 1224 1225 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1226 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1227 skippedLine = "| ... | ... | ... | ... |\n" 1228 1229 if resultsLen == 0: 1230 info.append("\nNo results\n") 1231 infoShort.append("\nNo results\n") 1232 uLogger.warning("No results. Try changing your search pattern.") 1233 1234 else: 1235 for iType in searchResults: 1236 iTypeValuesCount = len(searchResults[iType].values()) 1237 if iTypeValuesCount > 0: 1238 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1239 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1240 1241 for instrument in searchResults[iType].values(): 1242 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1243 instrument["type"], 1244 instrument["ticker"], 1245 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1246 instrument["figi"], 1247 )) 1248 1249 if iTypeValuesCount <= 5: 1250 infoShort.extend(info[-iTypeValuesCount:]) 1251 1252 else: 1253 infoShort.extend(info[-5:]) 1254 infoShort.append(skippedLine) 1255 1256 infoText = "".join(info) 1257 infoTextShort = "".join(infoShort) 1258 1259 if show: 1260 uLogger.info(infoTextShort) 1261 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1262 1263 if self.searchResultsFile: 1264 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1265 fH.write(infoText) 1266 1267 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1268 1269 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse— return list of result only.
Returns
list of dictionaries with all found instruments.
1271 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1272 """ 1273 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1274 1275 :param instruments: list of strings with tickers or FIGIs. 1276 :return: list with unique instrument FIGIs only. 1277 """ 1278 requestedInstruments = [] 1279 for iName in instruments: 1280 if iName not in self.aliases.keys(): 1281 if iName not in requestedInstruments: 1282 requestedInstruments.append(iName) 1283 1284 else: 1285 if iName not in requestedInstruments: 1286 if self.aliases[iName] not in requestedInstruments: 1287 requestedInstruments.append(self.aliases[iName]) 1288 1289 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1290 1291 onlyUniqueFIGIs = [] 1292 for iName in requestedInstruments: 1293 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1294 continue 1295 1296 self.ticker = iName 1297 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1298 1299 if not iData: 1300 self.ticker = "" 1301 self.figi = iName 1302 1303 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1304 1305 if not iData: 1306 self.figi = "" 1307 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1308 1309 if iData and iData["figi"] not in onlyUniqueFIGIs: 1310 onlyUniqueFIGIs.append(iData["figi"]) 1311 1312 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1313 1314 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1316 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1317 """ 1318 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1319 1320 See limits: https://tinkoff.github.io/investAPI/limits/ 1321 1322 If `pricesFile` string is not empty then also save information to this file. 1323 1324 :param instruments: list of strings with tickers or FIGIs. 1325 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1326 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1327 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1328 """ 1329 if instruments is None or not instruments: 1330 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1331 raise Exception("Ticker or FIGI required") 1332 1333 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1334 1335 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1336 1337 iList = [] # trying to get info and current prices about all unique instruments: 1338 for self.figi in onlyUniqueFIGIs: 1339 iData = self.SearchByFIGI(requestPrice=True) 1340 iList.append(iData) 1341 1342 self.ShowListOfPrices(iList, show) 1343 1344 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1346 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1347 """ 1348 Show table contains current prices of given instruments. 1349 1350 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1351 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1352 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1353 :return: multilines text in Markdown format as a table contains current prices. 1354 """ 1355 infoText = "" 1356 1357 if show or self.pricesFile: 1358 info = [ 1359 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1360 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1361 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1362 ] 1363 1364 for item in iList: 1365 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1366 item["ticker"], 1367 item["figi"], 1368 item["type"], 1369 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1370 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1371 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1372 "{} / {}".format( 1373 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1374 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1375 ), 1376 "{} / {}".format( 1377 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1378 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1379 ), 1380 item["currency"], 1381 )) 1382 1383 infoText = "".join(info) 1384 1385 if show: 1386 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1387 1388 if self.pricesFile: 1389 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1390 fH.write(infoText) 1391 1392 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1393 1394 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1396 def RequestTradingStatus(self) -> dict: 1397 """ 1398 Requesting trading status for the instrument defined by `figi` variable. 1399 1400 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1401 1402 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1403 1404 :return: dictionary with trading status attributes. Response example: 1405 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1406 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1407 """ 1408 if self.figi is None or not self.figi: 1409 uLogger.error("Variable `figi` must be defined for using this method!") 1410 raise Exception("FIGI required") 1411 1412 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1413 1414 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1415 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1416 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1417 1418 if self.moreDebug: 1419 uLogger.debug("Records about current trading status successfully received") 1420 1421 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1423 def RequestPortfolio(self) -> dict: 1424 """ 1425 Requesting actual user's portfolio for current `accountId`. 1426 1427 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1428 1429 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1430 1431 :return: dictionary with user's portfolio. 1432 """ 1433 if self.accountId is None or not self.accountId: 1434 uLogger.error("Variable `accountId` must be defined for using this method!") 1435 raise Exception("Account ID required") 1436 1437 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1438 1439 self.body = str({"accountId": self.accountId}) 1440 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1441 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1442 1443 if self.moreDebug: 1444 uLogger.debug("Records about user's portfolio successfully received") 1445 1446 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1448 def RequestPositions(self) -> dict: 1449 """ 1450 Requesting open positions by currencies and instruments for current `accountId`. 1451 1452 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1453 1454 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1455 1456 :return: dictionary with open positions by instruments. 1457 """ 1458 if self.accountId is None or not self.accountId: 1459 uLogger.error("Variable `accountId` must be defined for using this method!") 1460 raise Exception("Account ID required") 1461 1462 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1463 1464 self.body = str({"accountId": self.accountId}) 1465 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1466 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1467 1468 if self.moreDebug: 1469 uLogger.debug("Records about current open positions successfully received") 1470 1471 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1473 def RequestPendingOrders(self) -> list: 1474 """ 1475 Requesting current actual pending limit orders for current `accountId`. 1476 1477 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1478 1479 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1480 1481 :return: list of dictionaries with pending limit orders. 1482 """ 1483 if self.accountId is None or not self.accountId: 1484 uLogger.error("Variable `accountId` must be defined for using this method!") 1485 raise Exception("Account ID required") 1486 1487 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1488 1489 self.body = str({"accountId": self.accountId}) 1490 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1491 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1492 1493 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1494 1495 return rawOrders
Requesting current actual pending limit orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending limit orders.
1497 def RequestStopOrders(self) -> list: 1498 """ 1499 Requesting current actual stop orders for current `accountId`. 1500 1501 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1502 1503 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1504 1505 :return: list of dictionaries with stop orders. 1506 """ 1507 if self.accountId is None or not self.accountId: 1508 uLogger.error("Variable `accountId` must be defined for using this method!") 1509 raise Exception("Account ID required") 1510 1511 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1512 1513 self.body = str({"accountId": self.accountId}) 1514 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1515 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1516 1517 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1518 1519 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1521 def Overview(self, show: bool = False, details: str = "full") -> dict: 1522 """ 1523 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1524 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1525 and `overviewBondsCalendarFile` are defined then also save information to file. 1526 1527 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1528 many requests about the state of the portfolio, and then, based on the received data, a large number 1529 of calculation and statistics are collected. 1530 1531 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1532 :param details: how detailed should the information be? 1533 - `full` — shows full available information about portfolio status (by default), 1534 - `positions` — shows only open positions, 1535 - `orders` — shows only sections of open limits and stop orders. 1536 - `digest` — show a short digest of the portfolio status, 1537 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1538 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1539 :return: dictionary with client's raw portfolio and some statistics. 1540 """ 1541 if self.accountId is None or not self.accountId: 1542 uLogger.error("Variable `accountId` must be defined for using this method!") 1543 raise Exception("Account ID required") 1544 1545 view = { 1546 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1547 "headers": {}, # list of dictionaries, response headers without "positions" section 1548 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1549 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1550 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1551 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1552 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1553 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1554 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1555 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1556 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1557 }, 1558 "stat": { # --- some statistics calculated using "raw" sections: 1559 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1560 "availableRUB": 0., # available rubles (without other currencies) 1561 "blockedRUB": 0., # blocked sum in Russian Rouble 1562 "totalChangesRUB": 0., # changes for all open trades in RUB 1563 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1564 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1565 "sharesCostRUB": 0., # costs of all shares in RUB 1566 "bondsCostRUB": 0., # costs of all bonds in RUB 1567 "etfsCostRUB": 0., # costs of all etfs in RUB 1568 "futuresCostRUB": 0., # costs of all futures in RUB 1569 "Currencies": [], # list of dictionaries of all currencies statistics 1570 "Shares": [], # list of dictionaries of all shares statistics 1571 "Bonds": [], # list of dictionaries of all bonds statistics 1572 "Etfs": [], # list of dictionaries of all etfs statistics 1573 "Futures": [], # list of dictionaries of all futures statistics 1574 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1575 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1576 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1577 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1578 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1579 }, 1580 "analytics": { # --- some analytics of portfolio: 1581 "distrByAssets": {}, # portfolio distribution by assets 1582 "distrByCompanies": {}, # portfolio distribution by companies 1583 "distrBySectors": {}, # portfolio distribution by sectors 1584 "distrByCurrencies": {}, # portfolio distribution by currencies 1585 "distrByCountries": {}, # portfolio distribution by countries 1586 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1587 } 1588 } 1589 1590 details = details.lower() 1591 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1592 if details not in availableDetails: 1593 details = "full" 1594 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1595 1596 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1597 1598 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1599 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1600 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1601 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1602 1603 # save response headers without "positions" section: 1604 for key in portfolioResponse.keys(): 1605 if key != "positions": 1606 view["raw"]["headers"][key] = portfolioResponse[key] 1607 1608 else: 1609 continue 1610 1611 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1612 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1613 for item in portfolioResponse["positions"]: 1614 if item["instrumentType"] == "currency": 1615 self.figi = item["figi"] 1616 curr = self.SearchByFIGI(requestPrice=False) 1617 1618 # current price of currency in RUB: 1619 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1620 "name": curr["name"], 1621 "currentPrice": NanoToFloat( 1622 item["currentPrice"]["units"], 1623 item["currentPrice"]["nano"] 1624 ), 1625 } 1626 1627 view["raw"]["Currencies"].append(item) 1628 1629 elif item["instrumentType"] == "share": 1630 view["raw"]["Shares"].append(item) 1631 1632 elif item["instrumentType"] == "bond": 1633 view["raw"]["Bonds"].append(item) 1634 1635 elif item["instrumentType"] == "etf": 1636 view["raw"]["Etfs"].append(item) 1637 1638 elif item["instrumentType"] == "futures": 1639 view["raw"]["Futures"].append(item) 1640 1641 else: 1642 continue 1643 1644 # how many volume of currencies (by ISO currency name) are blocked: 1645 for item in view["raw"]["positions"]["blocked"]: 1646 blocked = NanoToFloat(item["units"], item["nano"]) 1647 if blocked > 0: 1648 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1649 1650 # how many volume of instruments (by FIGI) are blocked: 1651 for item in view["raw"]["positions"]["securities"]: 1652 blocked = int(item["blocked"]) 1653 if blocked > 0: 1654 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1655 1656 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1657 1658 if "rub" in allBlocked.keys(): 1659 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1660 1661 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1662 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1663 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1664 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1665 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1666 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1667 view["stat"]["portfolioCostRUB"] = sum([ 1668 view["stat"]["allCurrenciesCostRUB"], 1669 view["stat"]["sharesCostRUB"], 1670 view["stat"]["bondsCostRUB"], 1671 view["stat"]["etfsCostRUB"], 1672 view["stat"]["futuresCostRUB"], 1673 ]) 1674 1675 # --- calculating some portfolio statistics: 1676 byComp = {} # distribution by companies 1677 bySect = {} # distribution by sectors 1678 byCurr = {} # distribution by currencies (include RUB) 1679 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1680 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1681 1682 for item in portfolioResponse["positions"]: 1683 self.figi = item["figi"] 1684 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1685 1686 if instrument: 1687 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1688 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1689 1690 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1691 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1692 1693 else: 1694 blocked = 0 1695 1696 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1697 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1698 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1699 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1700 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1701 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1702 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1703 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1704 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1705 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1706 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1707 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1708 1709 statData = { 1710 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1711 "ticker": instrument["ticker"], # ticker by FIGI 1712 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1713 "volume": volume, # available volume of instrument 1714 "lots": lots, # volume in lots of instrument 1715 "direction": direction, # direction of an instrument's position: short or long 1716 "blocked": blocked, # blocked volume of currency or instrument 1717 "currentPrice": curPrice, # current instrument's price in basic asset 1718 "average": average, # current average position price 1719 "cost": cost, # current cost of all volume of instrument in basic asset 1720 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1721 "costRUB": costRUB, # cost of instrument in ruble 1722 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1723 "profit": profit, # expected profit at current moment 1724 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1725 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1726 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1727 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1728 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1729 "step": instrument["step"], # minimum price increment 1730 } 1731 1732 # adding distribution by unique countries: 1733 if statData["country"] not in byCountry.keys(): 1734 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1735 1736 else: 1737 byCountry[statData["country"]]["cost"] += costRUB 1738 byCountry[statData["country"]]["percent"] += percentCostRUB 1739 1740 if item["instrumentType"] != "currency": 1741 # adding distribution by unique companies: 1742 if statData["name"]: 1743 if statData["name"] not in byComp.keys(): 1744 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1745 1746 else: 1747 byComp[statData["name"]]["cost"] += costRUB 1748 byComp[statData["name"]]["percent"] += percentCostRUB 1749 1750 # adding distribution by unique sectors: 1751 if statData["sector"] not in bySect.keys(): 1752 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1753 1754 else: 1755 bySect[statData["sector"]]["cost"] += costRUB 1756 bySect[statData["sector"]]["percent"] += percentCostRUB 1757 1758 # adding distribution by unique currencies: 1759 if currency not in byCurr.keys(): 1760 byCurr[currency] = { 1761 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1762 "cost": costRUB, 1763 "percent": percentCostRUB 1764 } 1765 1766 else: 1767 byCurr[currency]["cost"] += costRUB 1768 byCurr[currency]["percent"] += percentCostRUB 1769 1770 # saving statistics for every instrument: 1771 if item["instrumentType"] == "currency": 1772 view["stat"]["Currencies"].append(statData) 1773 1774 # update dict with free funds for trading (total - blocked) by currencies 1775 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1776 view["stat"]["funds"][currency] = { 1777 "total": volume, 1778 "totalCostRUB": costRUB, # total volume cost in rubles 1779 "free": volume - blocked, 1780 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1781 } 1782 1783 elif item["instrumentType"] == "share": 1784 view["stat"]["Shares"].append(statData) 1785 1786 elif item["instrumentType"] == "bond": 1787 view["stat"]["Bonds"].append(statData) 1788 1789 elif item["instrumentType"] == "etf": 1790 view["stat"]["Etfs"].append(statData) 1791 1792 elif item["instrumentType"] == "Futures": 1793 view["stat"]["Futures"].append(statData) 1794 1795 else: 1796 continue 1797 1798 # total changes in Russian Ruble: 1799 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1800 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1801 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1802 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1803 view["stat"]["funds"]["rub"] = { 1804 "total": view["stat"]["availableRUB"], 1805 "totalCostRUB": view["stat"]["availableRUB"], 1806 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1807 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1808 } 1809 1810 # --- pending limit orders sector data: 1811 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1812 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1813 1814 for item in view["raw"]["orders"]: 1815 self.figi = item["figi"] 1816 1817 if item["figi"] not in uniquePendingOrdersFIGIs: 1818 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1819 1820 uniquePendingOrdersFIGIs.append(item["figi"]) 1821 uniquePendingOrders[item["figi"]] = instrument 1822 1823 else: 1824 instrument = uniquePendingOrders[item["figi"]] 1825 1826 if instrument: 1827 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1828 orderType = TKS_ORDER_TYPES[item["orderType"]] 1829 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1830 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1831 1832 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1833 if item["direction"] == "ORDER_DIRECTION_BUY": 1834 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1835 1836 else: 1837 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1838 1839 # requested price for order execution: 1840 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1841 1842 # necessary changes in percent to reach target from current price: 1843 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1844 1845 view["stat"]["orders"].append({ 1846 "orderID": item["orderId"], # orderId number parameter of current order 1847 "figi": item["figi"], # FIGI identification 1848 "ticker": instrument["ticker"], # ticker name by FIGI 1849 "lotsRequested": item["lotsRequested"], # requested lots value 1850 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1851 "currentPrice": lastPrice, # current instrument's price for defined action 1852 "targetPrice": target, # requested price for order execution in base currency 1853 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1854 "percentChanges": changes, # changes in percent to target from current price 1855 "currency": item["currency"], # instrument's currency name 1856 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1857 "type": orderType, # type of order from TKS_ORDER_TYPES 1858 "status": orderState, # order status from TKS_ORDER_STATES 1859 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1860 }) 1861 1862 # --- stop orders sector data: 1863 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1864 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1865 1866 for item in view["raw"]["stopOrders"]: 1867 self.figi = item["figi"] 1868 1869 if item["figi"] not in uniqueStopOrdersFIGIs: 1870 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1871 1872 uniqueStopOrdersFIGIs.append(item["figi"]) 1873 uniqueStopOrders[item["figi"]] = instrument 1874 1875 else: 1876 instrument = uniqueStopOrders[item["figi"]] 1877 1878 if instrument: 1879 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1880 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1881 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1882 1883 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1884 if "expirationTime" in item.keys(): 1885 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1886 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1887 1888 else: 1889 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1890 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1891 1892 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1893 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1894 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1895 1896 else: 1897 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1898 1899 # requested price when stop-order executed: 1900 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1901 1902 # price for limit-order, set up when stop-order executed: 1903 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1904 1905 # necessary changes in percent to reach target from current price: 1906 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1907 1908 view["stat"]["stopOrders"].append({ 1909 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1910 "figi": item["figi"], # FIGI identification 1911 "ticker": instrument["ticker"], # ticker name by FIGI 1912 "lotsRequested": item["lotsRequested"], # requested lots value 1913 "currentPrice": lastPrice, # current instrument's price for defined action 1914 "targetPrice": target, # requested price for stop-order execution in base currency 1915 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1916 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1917 "percentChanges": changes, # changes in percent to target from current price 1918 "currency": item["currency"], # instrument's currency name 1919 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1920 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1921 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1922 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1923 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1924 }) 1925 1926 # --- calculating data for analytics section: 1927 # portfolio distribution by assets: 1928 view["analytics"]["distrByAssets"] = { 1929 "Ruble": { 1930 "uniques": 1, 1931 "cost": view["stat"]["availableRUB"], 1932 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1933 }, 1934 "Currencies": { 1935 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1936 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1937 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1938 }, 1939 "Shares": { 1940 "uniques": len(view["stat"]["Shares"]), 1941 "cost": view["stat"]["sharesCostRUB"], 1942 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1943 }, 1944 "Bonds": { 1945 "uniques": len(view["stat"]["Bonds"]), 1946 "cost": view["stat"]["bondsCostRUB"], 1947 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1948 }, 1949 "Etfs": { 1950 "uniques": len(view["stat"]["Etfs"]), 1951 "cost": view["stat"]["etfsCostRUB"], 1952 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1953 }, 1954 "Futures": { 1955 "uniques": len(view["stat"]["Futures"]), 1956 "cost": view["stat"]["futuresCostRUB"], 1957 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1958 }, 1959 } 1960 1961 # portfolio distribution by companies: 1962 view["analytics"]["distrByCompanies"]["All money cash"] = { 1963 "ticker": "", 1964 "cost": view["stat"]["allCurrenciesCostRUB"], 1965 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1966 } 1967 view["analytics"]["distrByCompanies"].update(byComp) 1968 1969 # portfolio distribution by sectors: 1970 view["analytics"]["distrBySectors"]["All money cash"] = { 1971 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 1972 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 1973 } 1974 view["analytics"]["distrBySectors"].update(bySect) 1975 1976 # portfolio distribution by currencies: 1977 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 1978 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 1979 1980 if self.moreDebug: 1981 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 1982 1983 view["analytics"]["distrByCurrencies"].update(byCurr) 1984 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1985 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1986 1987 # portfolio distribution by countries: 1988 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 1989 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 1990 1991 if self.moreDebug: 1992 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 1993 1994 view["analytics"]["distrByCountries"].update(byCountry) 1995 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1996 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1997 1998 # --- Prepare text statistics overview in human-readable: 1999 if show: 2000 # Whatever the value `details`, header not changes: 2001 info = [ 2002 "# Client's portfolio\n\n", 2003 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2004 "* **Account ID:** [{}]\n".format(self.accountId), 2005 ] 2006 2007 if details in ["full", "positions", "digest"]: 2008 info.extend([ 2009 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2010 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2011 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2012 view["stat"]["totalChangesRUB"], 2013 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2014 view["stat"]["totalChangesPercentRUB"], 2015 ), 2016 ]) 2017 2018 if details in ["full", "positions"]: 2019 info.extend([ 2020 "## Open positions\n\n", 2021 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2022 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2023 "| Ruble | {:>31} | | | | | |\n".format( 2024 "{:.2f} ({:.2f}) rub".format( 2025 view["stat"]["availableRUB"], 2026 view["stat"]["blockedRUB"], 2027 ) 2028 ) 2029 ]) 2030 2031 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2032 return [ 2033 "| | | | | | | |\n", 2034 "| {:<27} | | | | | {:>19} | |\n".format( 2035 noTradeStr if noTradeStr else typeStr, 2036 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2037 ), 2038 ] 2039 2040 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2041 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2042 "{} [{}]".format(data["ticker"], data["figi"]), 2043 "{:.2f} ({:.2f}) {}".format( 2044 data["volume"], 2045 data["blocked"], 2046 data["currency"], 2047 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2048 data["volume"], 2049 data["blocked"], 2050 ), 2051 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2052 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2053 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2054 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2055 "{}{:.2f} {} ({}{:.2f}%)".format( 2056 "+" if data["profit"] > 0 else "", 2057 data["profit"], data["baseCurrencyName"], 2058 "+" if data["percentProfit"] > 0 else "", 2059 data["percentProfit"], 2060 ), 2061 ) 2062 2063 # --- Show currencies section: 2064 if view["stat"]["Currencies"]: 2065 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2066 for item in view["stat"]["Currencies"]: 2067 info.append(_InfoStr(item, showCurrencyName=True)) 2068 2069 else: 2070 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2071 2072 # --- Show shares section: 2073 if view["stat"]["Shares"]: 2074 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2075 2076 for item in view["stat"]["Shares"]: 2077 info.append(_InfoStr(item)) 2078 2079 else: 2080 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2081 2082 # --- Show bonds section: 2083 if view["stat"]["Bonds"]: 2084 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2085 2086 for item in view["stat"]["Bonds"]: 2087 info.append(_InfoStr(item)) 2088 2089 else: 2090 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2091 2092 # --- Show etfs section: 2093 if view["stat"]["Etfs"]: 2094 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2095 2096 for item in view["stat"]["Etfs"]: 2097 info.append(_InfoStr(item)) 2098 2099 else: 2100 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2101 2102 # --- Show futures section: 2103 if view["stat"]["Futures"]: 2104 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2105 2106 for item in view["stat"]["Futures"]: 2107 info.append(_InfoStr(item)) 2108 2109 else: 2110 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2111 2112 if details in ["full", "orders"]: 2113 # --- Show pending limit orders section: 2114 if view["stat"]["orders"]: 2115 info.extend([ 2116 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2117 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2118 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2119 ]) 2120 2121 for item in view["stat"]["orders"]: 2122 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2123 "{} [{}]".format(item["ticker"], item["figi"]), 2124 item["orderID"], 2125 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2126 "{} {} ({}{:.2f}%)".format( 2127 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2128 item["baseCurrencyName"], 2129 "+" if item["percentChanges"] > 0 else "", 2130 float(item["percentChanges"]), 2131 ), 2132 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2133 item["action"], 2134 item["type"], 2135 item["date"], 2136 )) 2137 2138 else: 2139 info.append("\n## Total pending limit-orders: 0\n") 2140 2141 # --- Show stop orders section: 2142 if view["stat"]["stopOrders"]: 2143 info.extend([ 2144 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2145 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2146 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2147 ]) 2148 2149 for item in view["stat"]["stopOrders"]: 2150 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2151 "{} [{}]".format(item["ticker"], item["figi"]), 2152 item["orderID"], 2153 item["lotsRequested"], 2154 "{} {} ({}{:.2f}%)".format( 2155 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2156 item["baseCurrencyName"], 2157 "+" if item["percentChanges"] > 0 else "", 2158 float(item["percentChanges"]), 2159 ), 2160 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2161 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2162 item["action"], 2163 item["type"], 2164 item["expType"], 2165 item["createDate"], 2166 item["expDate"], 2167 )) 2168 2169 else: 2170 info.append("\n## Total stop-orders: 0\n") 2171 2172 if details in ["full", "analytics"]: 2173 # -- Show analytics section: 2174 if view["stat"]["portfolioCostRUB"] > 0: 2175 info.extend([ 2176 "\n# Analytics\n" 2177 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2178 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2179 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2180 view["stat"]["totalChangesRUB"], 2181 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2182 view["stat"]["totalChangesPercentRUB"], 2183 ), 2184 "\n## Portfolio distribution by assets\n" 2185 "\n| Type | Uniques | Percent | Current cost |\n", 2186 "|------------------------------------|---------|---------|--------------------|\n", 2187 ]) 2188 2189 for key in view["analytics"]["distrByAssets"].keys(): 2190 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2191 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2192 key, 2193 view["analytics"]["distrByAssets"][key]["uniques"], 2194 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2195 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2196 )) 2197 2198 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2199 2200 info.extend([ 2201 "\n## Portfolio distribution by companies\n" 2202 "\n| Company | Percent | Current cost |\n", 2203 aSepLine, 2204 ]) 2205 2206 for company in view["analytics"]["distrByCompanies"].keys(): 2207 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2208 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2209 "{}{}".format( 2210 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2211 company, 2212 ), 2213 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2214 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2215 )) 2216 2217 info.extend([ 2218 "\n## Portfolio distribution by sectors\n" 2219 "\n| Sector | Percent | Current cost |\n", 2220 aSepLine, 2221 ]) 2222 2223 for sector in view["analytics"]["distrBySectors"].keys(): 2224 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2225 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2226 sector, 2227 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2228 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2229 )) 2230 2231 info.extend([ 2232 "\n## Portfolio distribution by currencies\n" 2233 "\n| Instruments currencies | Percent | Current cost |\n", 2234 aSepLine, 2235 ]) 2236 2237 for curr in view["analytics"]["distrByCurrencies"].keys(): 2238 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2239 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2240 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2241 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2242 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2243 )) 2244 2245 info.extend([ 2246 "\n## Portfolio distribution by countries\n" 2247 "\n| Assets by country | Percent | Current cost |\n", 2248 aSepLine, 2249 ]) 2250 2251 for country in view["analytics"]["distrByCountries"].keys(): 2252 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2253 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2254 country, 2255 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2256 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2257 )) 2258 2259 if details in ["full", "calendar"]: 2260 # -- Show bonds payment calendar section: 2261 if view["stat"]["Bonds"]: 2262 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2263 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2264 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2265 2266 else: 2267 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2268 2269 infoText = "".join(info) 2270 2271 uLogger.info(infoText) 2272 2273 if details == "full" and self.overviewFile: 2274 filename = self.overviewFile 2275 2276 elif details == "digest" and self.overviewDigestFile: 2277 filename = self.overviewDigestFile 2278 2279 elif details == "positions" and self.overviewPositionsFile: 2280 filename = self.overviewPositionsFile 2281 2282 elif details == "orders" and self.overviewOrdersFile: 2283 filename = self.overviewOrdersFile 2284 2285 elif details == "analytics" and self.overviewAnalyticsFile: 2286 filename = self.overviewAnalyticsFile 2287 2288 elif details == "calendar" and self.overviewBondsCalendarFile: 2289 filename = self.overviewBondsCalendarFile 2290 2291 else: 2292 filename = "" 2293 2294 if filename: 2295 with open(filename, "w", encoding="UTF-8") as fH: 2296 fH.write(infoText) 2297 2298 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2299 2300 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
and overviewBondsCalendarFile are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be?
full— shows full available information about portfolio status (by default),positions— shows only open positions,orders— shows only sections of open limits and stop orders.digest— show a short digest of the portfolio status,analytics— shows only the analytics section and the distribution of the portfolio by various categories,calendar— shows only the bonds calendar section (if these present in portfolio),
Returns
dictionary with client's raw portfolio and some statistics.
2302 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2303 """ 2304 Returns history operations between two given dates for current `accountId`. 2305 If `reportFile` string is not empty then also save human-readable report. 2306 Shows some statistical data of closed positions. 2307 2308 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2309 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2310 :param show: if `True` then also prints all records to the console. 2311 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2312 :return: original list of dictionaries with history of deals records from API ("operations" key): 2313 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2314 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2315 """ 2316 if self.accountId is None or not self.accountId: 2317 uLogger.error("Variable `accountId` must be defined for using this method!") 2318 raise Exception("Account ID required") 2319 2320 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2321 2322 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2323 2324 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2325 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2326 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2327 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2328 customStat = {} # custom statistics in additional to responseJSON 2329 2330 # --- output report in human-readable format: 2331 if show or self.reportFile: 2332 splitLine1 = "| | | | | |\n" # Summary section 2333 splitLine2 = "| | | | | | | | |\n" # Operations section 2334 nextDay = "" 2335 2336 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2337 2338 if len(ops) > 0: 2339 customStat = { 2340 "opsCount": 0, # total operations count 2341 "buyCount": 0, # buy operations 2342 "sellCount": 0, # sell operations 2343 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2344 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2345 "payIn": {"rub": 0.}, # Deposit brokerage account 2346 "payOut": {"rub": 0.}, # Withdrawals 2347 "divs": {"rub": 0.}, # Dividends income 2348 "coupons": {"rub": 0.}, # Coupon's income 2349 "brokerCom": {"rub": 0.}, # Service commissions 2350 "serviceCom": {"rub": 0.}, # Service commissions 2351 "marginCom": {"rub": 0.}, # Margin commissions 2352 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2353 } 2354 2355 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2356 for item in ops: 2357 if item["state"] == "OPERATION_STATE_EXECUTED": 2358 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2359 2360 # count buy operations: 2361 if "_BUY" in item["operationType"]: 2362 customStat["buyCount"] += 1 2363 2364 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2365 customStat["buyTotal"][item["payment"]["currency"]] += payment 2366 2367 else: 2368 customStat["buyTotal"][item["payment"]["currency"]] = payment 2369 2370 # count sell operations: 2371 elif "_SELL" in item["operationType"]: 2372 customStat["sellCount"] += 1 2373 2374 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2375 customStat["sellTotal"][item["payment"]["currency"]] += payment 2376 2377 else: 2378 customStat["sellTotal"][item["payment"]["currency"]] = payment 2379 2380 # count incoming operations: 2381 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2382 if item["payment"]["currency"] in customStat["payIn"].keys(): 2383 customStat["payIn"][item["payment"]["currency"]] += payment 2384 2385 else: 2386 customStat["payIn"][item["payment"]["currency"]] = payment 2387 2388 # count withdrawals operations: 2389 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2390 if item["payment"]["currency"] in customStat["payOut"].keys(): 2391 customStat["payOut"][item["payment"]["currency"]] += payment 2392 2393 else: 2394 customStat["payOut"][item["payment"]["currency"]] = payment 2395 2396 # count dividends income: 2397 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2398 if item["payment"]["currency"] in customStat["divs"].keys(): 2399 customStat["divs"][item["payment"]["currency"]] += payment 2400 2401 else: 2402 customStat["divs"][item["payment"]["currency"]] = payment 2403 2404 # count coupon's income: 2405 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2406 if item["payment"]["currency"] in customStat["coupons"].keys(): 2407 customStat["coupons"][item["payment"]["currency"]] += payment 2408 2409 else: 2410 customStat["coupons"][item["payment"]["currency"]] = payment 2411 2412 # count broker commissions: 2413 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2414 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2415 customStat["brokerCom"][item["payment"]["currency"]] += payment 2416 2417 else: 2418 customStat["brokerCom"][item["payment"]["currency"]] = payment 2419 2420 # count service commissions: 2421 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2422 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2423 customStat["serviceCom"][item["payment"]["currency"]] += payment 2424 2425 else: 2426 customStat["serviceCom"][item["payment"]["currency"]] = payment 2427 2428 # count margin commissions: 2429 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2430 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2431 customStat["marginCom"][item["payment"]["currency"]] += payment 2432 2433 else: 2434 customStat["marginCom"][item["payment"]["currency"]] = payment 2435 2436 # count withholding taxes: 2437 elif "_TAX" in item["operationType"]: 2438 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2439 customStat["allTaxes"][item["payment"]["currency"]] += payment 2440 2441 else: 2442 customStat["allTaxes"][item["payment"]["currency"]] = payment 2443 2444 else: 2445 continue 2446 2447 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2448 2449 # --- view "Actions" lines: 2450 info.extend([ 2451 "| Report sections | | | | |\n", 2452 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2453 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2454 "| | Buy: {:<22} | {:<28} | | |\n".format( 2455 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2456 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2457 ), 2458 "| | Sell: {:<21} | {:<28} | | |\n".format( 2459 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2460 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2461 ), 2462 ]) 2463 2464 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2465 for key in opsKeys: 2466 if key == "rub": 2467 continue 2468 2469 info.extend([ 2470 "| | | {:<28} | | |\n".format( 2471 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2472 ), 2473 "| | | {:<28} | | |\n".format( 2474 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2475 ), 2476 ]) 2477 2478 info.append(splitLine1) 2479 2480 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2481 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2482 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2483 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2484 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2485 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2486 ) 2487 2488 # --- view "Payments" lines: 2489 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2490 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2491 2492 for key in paymentsKeys: 2493 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2494 2495 info.append(splitLine1) 2496 2497 # --- view "Commissions and taxes" lines: 2498 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2499 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2500 2501 for key in comKeys: 2502 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2503 2504 info.append(splitLine1) 2505 2506 info.extend([ 2507 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2508 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2509 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2510 ]) 2511 2512 else: 2513 info.append("Broker returned no operations during this period\n") 2514 2515 # --- view "Operations" section: 2516 for item in ops: 2517 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2518 continue 2519 2520 else: 2521 self.figi = item["figi"] if item["figi"] else "" 2522 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2523 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2524 2525 # group of deals during one day: 2526 if nextDay and item["date"].split("T")[0] != nextDay: 2527 info.append(splitLine2) 2528 nextDay = "" 2529 2530 else: 2531 nextDay = item["date"].split("T")[0] # saving current day for splitting 2532 2533 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2534 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2535 self.figi if self.figi else "—", 2536 instrument["ticker"] if instrument else "—", 2537 instrument["type"] if instrument else "—", 2538 item["quantity"] if int(item["quantity"]) > 0 else "—", 2539 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2540 TKS_OPERATION_STATES[item["state"]], 2541 TKS_OPERATION_TYPES[item["operationType"]], 2542 )) 2543 2544 infoText = "".join(info) 2545 2546 if show: 2547 if self.moreDebug: 2548 uLogger.debug("Records about history of a client's operations successfully received") 2549 2550 uLogger.info(infoText) 2551 2552 if self.reportFile: 2553 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2554 fH.write(infoText) 2555 2556 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2557 2558 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2560 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2561 """ 2562 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2563 2564 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2565 Warning! Broker server used ISO UTC time by default. 2566 2567 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2568 Also, `historyFile` used to update history with `onlyMissing` parameter. 2569 2570 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2571 2572 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2573 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2574 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2575 `"hour"`, `"day"`. Default: `"hour"`. 2576 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2577 False by default. Warning! History appends only from last candle to current time 2578 with always update last candle! 2579 :param csvSep: separator if csv-file is used, `,` by default. 2580 :param show: if `True` then also prints Pandas DataFrame to the console. 2581 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2582 `["date", "time", "open", "high", "low", "close", "volume"]`. 2583 """ 2584 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2585 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2586 history = None # empty pandas object for history 2587 2588 if interval not in TKS_CANDLE_INTERVALS.keys(): 2589 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2590 raise Exception("Incorrect value") 2591 2592 if not (self.ticker or self.figi): 2593 uLogger.error("Ticker or FIGI must be defined!") 2594 raise Exception("Ticker or FIGI required") 2595 2596 if self.ticker and not self.figi: 2597 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2598 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2599 2600 if self.figi and not self.ticker: 2601 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2602 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2603 2604 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2605 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2606 if interval.lower() != "day": 2607 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2608 2609 delta = dtEnd - dtStart # current UTC time minus last time in file 2610 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2611 2612 # calculate history length in candles: 2613 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2614 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2615 length += 1 # to avoid fraction time 2616 2617 # calculate data blocks count: 2618 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2619 2620 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2621 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2622 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2623 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2624 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2625 2626 tempOld = None # pandas object for old history, if --only-missing key present 2627 lastTime = None # datetime object of last old candle in file 2628 2629 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2630 uLogger.debug("--only-missing key present, add only last missing candles...") 2631 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2632 2633 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2634 2635 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2636 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2637 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2638 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2639 2640 # get last datetime object from last string in file or minus 1 delta if file is empty: 2641 if len(tempOld) > 0: 2642 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2643 2644 else: 2645 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2646 2647 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2648 2649 responseJSONs = [] # raw history blocks of data 2650 2651 blockEnd = dtEnd 2652 for item in range(blocks): 2653 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2654 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2655 2656 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2657 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2658 )) 2659 2660 if blockStart == blockEnd: 2661 uLogger.debug("Skipped this zero-length block...") 2662 2663 else: 2664 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2665 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2666 self.body = str({ 2667 "figi": self.figi, 2668 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2669 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2670 "interval": TKS_CANDLE_INTERVALS[interval][0] 2671 }) 2672 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2673 2674 if "code" in responseJSON.keys(): 2675 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2676 2677 else: 2678 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2679 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2680 2681 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2682 2683 blockEnd = blockStart 2684 2685 printCount = len(responseJSONs) # candles to show in console 2686 if responseJSONs: 2687 tempHistory = pd.DataFrame( 2688 data={ 2689 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2690 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2691 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2692 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2693 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2694 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2695 "volume": [int(item["volume"]) for item in responseJSONs], 2696 }, 2697 index=range(len(responseJSONs)), 2698 columns=["date", "time", "open", "high", "low", "close", "volume"], 2699 ) 2700 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2701 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2702 2703 # append only newest candles to old history if --only-missing key present: 2704 if onlyMissing and tempOld is not None and lastTime is not None: 2705 index = 0 # find start index in tempHistory data: 2706 2707 for i, item in tempHistory.iterrows(): 2708 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2709 2710 if curTime == lastTime: 2711 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2712 index = i 2713 printCount = index + 1 2714 break 2715 2716 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2717 2718 else: 2719 history = tempHistory # if no `--only-missing` key then load full data from server 2720 2721 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2722 2723 if history is not None and not history.empty: 2724 if show: 2725 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2726 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2727 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2728 )) 2729 2730 else: 2731 uLogger.warning("Received an empty candles history!") 2732 2733 if self.historyFile is not None: 2734 if history is not None and not history.empty: 2735 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2736 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2737 2738 else: 2739 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2740 2741 else: 2742 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2743 2744 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2746 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2747 """ 2748 Load candles history from csv-file and return Pandas DataFrame object. 2749 2750 See also: `History()` and `ShowHistoryChart()` methods. 2751 2752 :param filePath: path to csv-file to open. 2753 """ 2754 loadedHistory = None # init candles data object 2755 2756 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2757 2758 if os.path.exists(filePath): 2759 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2760 2761 tfStr = self.priceModel.FormattedDelta( 2762 self.priceModel.timeframe, 2763 "{days} days {hours}h {minutes}m {seconds}s", 2764 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2765 self.priceModel.timeframe, 2766 "{hours}h {minutes}m {seconds}s", 2767 ) 2768 2769 if loadedHistory is not None and not loadedHistory.empty: 2770 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2771 len(loadedHistory), 2772 tfStr, 2773 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2774 ) 2775 2776 else: 2777 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2778 2779 else: 2780 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2781 2782 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2784 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2785 """ 2786 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2787 2788 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2789 Default: `index.html` (both for interact and non-interact candlesticks chart). 2790 2791 See also: `History()` and `LoadHistory()` methods. 2792 2793 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2794 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2795 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2796 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2797 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2798 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2799 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2800 """ 2801 if isinstance(candles, str): 2802 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2803 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2804 2805 elif isinstance(candles, pd.DataFrame): 2806 self.priceModel.prices = candles # set candles chain from variable 2807 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2808 2809 if "datetime" not in candles.columns: 2810 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2811 2812 else: 2813 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2814 raise Exception("Incorrect value") 2815 2816 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2817 2818 if interact: 2819 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2820 2821 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2822 2823 else: 2824 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2825 2826 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2827 2828 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2830 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2831 """ 2832 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2833 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2834 2835 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2836 2837 :param operation: string "Buy" or "Sell". 2838 :param lots: volume, integer count of lots >= 1. 2839 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2840 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2841 :param expDate: string "Undefined" by default or local date in future, 2842 it is a string with format `%Y-%m-%d %H:%M:%S`. 2843 :return: JSON with response from broker server. 2844 """ 2845 if self.accountId is None or not self.accountId: 2846 uLogger.error("Variable `accountId` must be defined for using this method!") 2847 raise Exception("Account ID required") 2848 2849 if operation is None or not operation or operation not in ("Buy", "Sell"): 2850 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2851 raise Exception("Incorrect value") 2852 2853 if lots is None or lots < 1: 2854 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2855 lots = 1 2856 2857 if tp is None or tp < 0: 2858 tp = 0 2859 2860 if sl is None or sl < 0: 2861 sl = 0 2862 2863 if expDate is None or not expDate: 2864 expDate = "Undefined" 2865 2866 if not (self.ticker or self.figi): 2867 uLogger.error("Ticker or FIGI must be defined!") 2868 raise Exception("Ticker or FIGI required") 2869 2870 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2871 self.ticker = instrument["ticker"] 2872 self.figi = instrument["figi"] 2873 2874 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2875 2876 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2877 self.body = str({ 2878 "figi": self.figi, 2879 "quantity": str(lots), 2880 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2881 "accountId": str(self.accountId), 2882 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2883 }) 2884 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2885 2886 if "orderId" in response.keys(): 2887 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2888 operation, response["orderId"], 2889 self.ticker, self.figi, lots, 2890 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2891 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2892 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2893 )) 2894 2895 if tp > 0: 2896 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2897 2898 if sl > 0: 2899 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2900 2901 else: 2902 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 2903 2904 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2906 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2907 """ 2908 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2909 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2910 2911 See also: `Order()` and `Trade()` docstrings. 2912 2913 :param lots: volume, integer count of lots >= 1. 2914 :param tp: float > 0, take profit price of stop-order. 2915 :param sl: float > 0, stop loss price of stop-order. 2916 :param expDate: it's a local date in future. 2917 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2918 :return: JSON with response from broker server. 2919 """ 2920 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2922 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2923 """ 2924 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2925 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2926 2927 See also: `Order()` and `Trade()` docstrings. 2928 2929 :param lots: volume, integer count of lots >= 1. 2930 :param tp: float > 0, take profit price of stop-order. 2931 :param sl: float > 0, stop loss price of stop-order. 2932 :param expDate: it's a local date in the future. 2933 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2934 :return: JSON with response from broker server. 2935 """ 2936 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2938 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 2939 """ 2940 Close position of given instruments. 2941 2942 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 2943 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2944 This avoids unnecessary downloading data from the server. 2945 """ 2946 if instruments is None or not instruments: 2947 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 2948 raise Exception("Ticker or FIGI required") 2949 2950 if isinstance(instruments, str): 2951 instruments = [instruments] 2952 2953 uniqueInstruments = self.GetUniqueFIGIs(instruments) 2954 if uniqueInstruments: 2955 if portfolio is None or not portfolio: 2956 portfolio = self.Overview(show=False) 2957 2958 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2959 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 2960 2961 for self.figi in uniqueInstruments: 2962 if self.figi not in allOpened: 2963 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 2964 continue 2965 2966 # search open trade info about instrument by ticker: 2967 instrument = {} 2968 for iType in TKS_INSTRUMENTS: 2969 if instrument: 2970 break 2971 2972 for item in portfolio["stat"][iType]: 2973 if item["figi"] == self.figi: 2974 instrument = item 2975 break 2976 2977 if instrument: 2978 self.ticker = instrument["ticker"] 2979 self.figi = instrument["figi"] 2980 2981 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 2982 self.ticker, 2983 self.figi, 2984 int(instrument["volume"]), 2985 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 2986 )) 2987 2988 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 2989 2990 if tradeLots > 0: 2991 if instrument["blocked"] > 0: 2992 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 2993 instrument["blocked"], 2994 self.ticker, 2995 tradeLots, 2996 )) 2997 2998 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 2999 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3000 3001 else: 3002 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3004 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3005 """ 3006 Close all positions of given instruments with defined type. 3007 3008 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3009 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3010 This avoids unnecessary downloading data from the server. 3011 """ 3012 if iType not in TKS_INSTRUMENTS: 3013 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3014 3015 else: 3016 if portfolio is None or not portfolio: 3017 portfolio = self.Overview(show=False) 3018 3019 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3020 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3021 3022 if tickers and portfolio: 3023 self.CloseTrades(tickers, portfolio) 3024 3025 else: 3026 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3028 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3029 """ 3030 Universal method to create market or limit orders with all available parameters for current `accountId`. 3031 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3032 3033 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3034 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3035 3036 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3037 then broker immediately open market order as you can do simple --buy or --sell operations! 3038 3039 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3040 When current price will go up or down to target price value then broker opens a limit order. 3041 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3042 3043 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3044 3045 :param operation: string "Buy" or "Sell". 3046 :param orderType: string "Limit" or "Stop". 3047 :param lots: volume, integer count of lots >= 1. 3048 :param targetPrice: target price > 0. This is open trade price for limit order. 3049 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3050 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3051 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3052 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3053 Stop loss order always executed by market price. 3054 :param expDate: string "Undefined" by default or local date in future. 3055 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3056 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3057 A limit order has no expiration date, it lasts until the end of the trading day. 3058 :return: JSON with response from broker server. 3059 """ 3060 if self.accountId is None or not self.accountId: 3061 uLogger.error("Variable `accountId` must be defined for using this method!") 3062 raise Exception("Account ID required") 3063 3064 if operation is None or not operation or operation not in ("Buy", "Sell"): 3065 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3066 raise Exception("Incorrect value") 3067 3068 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3069 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3070 raise Exception("Incorrect value") 3071 3072 if lots is None or lots < 1: 3073 uLogger.error("You must define trade volume > 0: integer count of lots!") 3074 raise Exception("Incorrect value") 3075 3076 if targetPrice is None or targetPrice <= 0: 3077 uLogger.error("Target price for limit-order must be greater than 0!") 3078 raise Exception("Incorrect value") 3079 3080 if limitPrice is None or limitPrice <= 0: 3081 limitPrice = targetPrice 3082 3083 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3084 stopType = "Limit" 3085 3086 if expDate is None or not expDate: 3087 expDate = "Undefined" 3088 3089 if not (self.ticker or self.figi): 3090 uLogger.error("Tocker or FIGI must be defined!") 3091 raise Exception("Ticker or FIGI required") 3092 3093 response = {} 3094 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3095 self.ticker = instrument["ticker"] 3096 self.figi = instrument["figi"] 3097 3098 if orderType == "Limit": 3099 uLogger.debug( 3100 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3101 self.ticker, self.figi, 3102 operation, lots, targetPrice, instrument["currency"], 3103 )) 3104 3105 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3106 self.body = str({ 3107 "figi": self.figi, 3108 "quantity": str(lots), 3109 "price": FloatToNano(targetPrice), 3110 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3111 "accountId": str(self.accountId), 3112 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3113 }) 3114 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3115 3116 if "orderId" in response.keys(): 3117 uLogger.info( 3118 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3119 response["orderId"], 3120 self.ticker, self.figi, 3121 operation, lots, targetPrice, instrument["currency"], 3122 )) 3123 3124 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3125 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3126 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3127 targetPrice, instrument["currency"], 3128 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3129 )) 3130 3131 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3132 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3133 targetPrice, instrument["currency"], 3134 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3135 )) 3136 3137 else: 3138 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3139 3140 if orderType == "Stop": 3141 uLogger.debug( 3142 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3143 self.ticker, self.figi, 3144 operation, lots, 3145 targetPrice, instrument["currency"], 3146 limitPrice, instrument["currency"], 3147 stopType, expDate, 3148 )) 3149 3150 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3151 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3152 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3153 3154 body = { 3155 "figi": self.figi, 3156 "quantity": str(lots), 3157 "price": FloatToNano(limitPrice), 3158 "stopPrice": FloatToNano(targetPrice), 3159 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3160 "accountId": str(self.accountId), 3161 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3162 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3163 } 3164 3165 if expDateUTC: 3166 body["expireDate"] = expDateUTC 3167 3168 self.body = str(body) 3169 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3170 3171 if "stopOrderId" in response.keys(): 3172 uLogger.info( 3173 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3174 response["stopOrderId"], 3175 self.ticker, self.figi, 3176 operation, lots, 3177 targetPrice, instrument["currency"], 3178 limitPrice, instrument["currency"], 3179 TKS_STOP_ORDER_TYPES[stopOrderType], 3180 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3181 )) 3182 3183 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3184 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3185 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3186 targetPrice, instrument["currency"], 3187 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3188 )) 3189 3190 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3191 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3192 targetPrice, instrument["currency"], 3193 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3194 )) 3195 3196 else: 3197 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3198 3199 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3201 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3202 """ 3203 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3204 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3205 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3206 See also: `Order()` docstring. 3207 3208 :param lots: volume, integer count of lots >= 1. 3209 :param targetPrice: target price > 0. This is open trade price for limit order. 3210 :return: JSON with response from broker server. 3211 """ 3212 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3214 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3215 """ 3216 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3217 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3218 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3219 target price value then broker opens a limit order. See also: `Order()` docstring. 3220 3221 :param lots: volume, integer count of lots >= 1. 3222 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3223 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3224 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3225 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3226 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3227 :param expDate: string "Undefined" by default or local date in future. 3228 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3229 This date is converting to UTC format for server. 3230 :return: JSON with response from broker server. 3231 """ 3232 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3234 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3235 """ 3236 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3237 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3238 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3239 See also: `Order()` docstring. 3240 3241 :param lots: volume, integer count of lots >= 1. 3242 :param targetPrice: target price > 0. This is open trade price for limit order. 3243 :return: JSON with response from broker server. 3244 """ 3245 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3247 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3248 """ 3249 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3250 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3251 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3252 target price value then broker opens a limit order. See also: `Order()` docstring. 3253 3254 :param lots: volume, integer count of lots >= 1. 3255 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3256 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3257 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3258 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3259 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3260 :param expDate: string "Undefined" by default or local date in future. 3261 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3262 This date is converting to UTC format for server. 3263 :return: JSON with response from broker server. 3264 """ 3265 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3267 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3268 """ 3269 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3270 3271 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3272 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3273 This avoids unnecessary downloading data from the server. 3274 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3275 """ 3276 if self.accountId is None or not self.accountId: 3277 uLogger.error("Variable `accountId` must be defined for using this method!") 3278 raise Exception("Account ID required") 3279 3280 if orderIDs: 3281 if allOrdersIDs is None: 3282 rawOrders = self.RequestPendingOrders() 3283 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3284 3285 if allStopOrdersIDs is None: 3286 rawStopOrders = self.RequestStopOrders() 3287 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3288 3289 for orderID in orderIDs: 3290 idInPendingOrders = orderID in allOrdersIDs 3291 idInStopOrders = orderID in allStopOrdersIDs 3292 3293 if not (idInPendingOrders or idInStopOrders): 3294 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3295 continue 3296 3297 else: 3298 if idInPendingOrders: 3299 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3300 3301 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3302 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3303 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3304 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3305 3306 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3307 if self.moreDebug: 3308 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3309 3310 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3311 3312 else: 3313 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3314 3315 elif idInStopOrders: 3316 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3317 3318 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3319 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3320 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3321 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3322 3323 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3324 if self.moreDebug: 3325 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3326 3327 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3328 3329 else: 3330 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3331 3332 else: 3333 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3335 def CloseAllOrders(self) -> None: 3336 """ 3337 Gets a list of open pending and stop orders and cancel it all. 3338 """ 3339 rawOrders = self.RequestPendingOrders() 3340 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3341 lenOrders = len(allOrdersIDs) 3342 3343 rawStopOrders = self.RequestStopOrders() 3344 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3345 lenSOrders = len(allStopOrdersIDs) 3346 3347 if lenOrders > 0 or lenSOrders > 0: 3348 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3349 3350 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3351 3352 else: 3353 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3355 def CloseAll(self, *args) -> None: 3356 """ 3357 Close all available (not blocked) opened trades and orders. 3358 3359 Also, you can select one or more keywords case-insensitive: 3360 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3361 3362 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3363 """ 3364 overview = self.Overview(show=False) # get all open trades info 3365 3366 if len(args) == 0: 3367 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3368 self.CloseAllOrders() # close all pending and stop orders 3369 3370 for iType in TKS_INSTRUMENTS: 3371 if iType != "Currencies": 3372 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3373 3374 else: 3375 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3376 lowerArgs = [x.lower() for x in args] 3377 3378 if "orders" in lowerArgs: 3379 self.CloseAllOrders() # close all pending and stop orders 3380 3381 for iType in TKS_INSTRUMENTS: 3382 if iType.lower() in lowerArgs and iType != "Currencies": 3383 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3385 def CloseAllByTicker(self, instrument: str) -> None: 3386 """ 3387 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3388 3389 This method searches opened trade and orders of instrument throw all portfolio and then use 3390 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3391 3392 :param instrument: string with ticker. 3393 """ 3394 if instrument is None or not instrument: 3395 uLogger.error("Ticker name must be defined for using this method!") 3396 raise Exception("Ticker required") 3397 3398 overview = self.Overview(show=False) # get user portfolio with all open trades info 3399 3400 self.ticker = instrument # try to set instrument as ticker 3401 self.figi = "" 3402 3403 if self.IsInPortfolio(portfolio=overview): 3404 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3405 self.CloseTrades(instruments=[instrument], portfolio=overview) 3406 3407 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3408 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3409 3410 if limitAll and self.IsInLimitOrders(portfolio=overview): 3411 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3412 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3413 3414 if stopAll and self.IsInStopOrders(portfolio=overview): 3415 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3416 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
Parameters
- instrument: string with ticker.
3418 def CloseAllByFIGI(self, instrument: str) -> None: 3419 """ 3420 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3421 3422 This method searches opened trade and orders of instrument throw all portfolio and then use 3423 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3424 3425 :param instrument: string with FIGI id. 3426 """ 3427 if instrument is None or not instrument: 3428 uLogger.error("FIGI id must be defined for using this method!") 3429 raise Exception("FIGI required") 3430 3431 overview = self.Overview(show=False) # get user portfolio with all open trades info 3432 3433 self.ticker = "" 3434 self.figi = instrument # try to set instrument as FIGI id 3435 3436 if self.IsInPortfolio(portfolio=overview): 3437 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3438 self.CloseTrades(instruments=[instrument], portfolio=overview) 3439 3440 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3441 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3442 3443 if limitAll and self.IsInLimitOrders(portfolio=overview): 3444 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3445 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3446 3447 if stopAll and self.IsInStopOrders(portfolio=overview): 3448 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3449 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
Parameters
- instrument: string with FIGI id.
3451 @staticmethod 3452 def ParseOrderParameters(operation, **inputParameters): 3453 """ 3454 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3455 3456 :param operation: string "Buy" or "Sell". 3457 :param inputParameters: this is dict of strings that looks like this 3458 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3459 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3460 "prices" key: one or more prices to open limit-orders 3461 Counts of values in lots and prices lists must be equals! 3462 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3463 """ 3464 # TODO: update order grid work with api v2 3465 pass 3466 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3467 # 3468 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3469 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3470 # raise Exception("Incorrect value") 3471 # 3472 # if "l" in inputParameters.keys(): 3473 # inputParameters["lots"] = inputParameters.pop("l") 3474 # 3475 # if "p" in inputParameters.keys(): 3476 # inputParameters["prices"] = inputParameters.pop("p") 3477 # 3478 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3479 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3480 # raise Exception("Incorrect value") 3481 # 3482 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3483 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3484 # 3485 # if len(lots) != len(prices): 3486 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3487 # raise Exception("Incorrect value") 3488 # 3489 # uLogger.debug("Extracted parameters for orders:") 3490 # uLogger.debug("lots = {}".format(lots)) 3491 # uLogger.debug("prices = {}".format(prices)) 3492 # 3493 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3494 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3495 # uLogger.debug("Order parameters: {}".format(result)) 3496 # 3497 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3499 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3500 """ 3501 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3502 3503 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3504 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3505 """ 3506 result = False 3507 msg = "Instrument not defined!" 3508 3509 if portfolio is None or not portfolio: 3510 portfolio = self.Overview(show=False) 3511 3512 if self.ticker: 3513 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self.ticker)) 3514 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3515 3516 for iType in TKS_INSTRUMENTS: 3517 for instrument in portfolio["stat"][iType]: 3518 if instrument["ticker"] == self.ticker: 3519 result = True 3520 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3521 break 3522 3523 elif self.figi: 3524 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self.figi)) 3525 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3526 3527 for iType in TKS_INSTRUMENTS: 3528 for instrument in portfolio["stat"][iType]: 3529 if instrument["figi"] == self.figi: 3530 result = True 3531 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3532 break 3533 3534 else: 3535 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3536 3537 uLogger.debug(msg) 3538 3539 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3541 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3542 """ 3543 Returns instrument from the user's portfolio if it presents there. 3544 Instrument must be defined by `ticker` (highly priority) or `figi`. 3545 3546 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3547 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3548 """ 3549 result = None 3550 msg = "Instrument not defined!" 3551 3552 if portfolio is None or not portfolio: 3553 portfolio = self.Overview(show=False) 3554 3555 if self.ticker: 3556 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker)) 3557 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3558 3559 for iType in TKS_INSTRUMENTS: 3560 for instrument in portfolio["stat"][iType]: 3561 if instrument["ticker"] == self.ticker: 3562 result = instrument 3563 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3564 break 3565 3566 elif self.figi: 3567 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3568 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3569 3570 for iType in TKS_INSTRUMENTS: 3571 for instrument in portfolio["stat"][iType]: 3572 if instrument["figi"] == self.figi: 3573 result = instrument 3574 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3575 break 3576 3577 else: 3578 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3579 3580 uLogger.debug(msg) 3581 3582 return result
Returns instrument from the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3584 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3585 """ 3586 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3587 3588 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3589 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3590 """ 3591 result = False 3592 msg = "Instrument not defined!" 3593 3594 if portfolio is None or not portfolio: 3595 portfolio = self.Overview(show=False) 3596 3597 if self.ticker: 3598 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self.ticker)) 3599 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self.ticker) 3600 3601 for instrument in portfolio["stat"]["orders"]: 3602 if instrument["ticker"] == self.ticker: 3603 result = True 3604 msg = "Instrument with ticker [{}] is present in limit orders list".format(self.ticker) 3605 break 3606 3607 elif self.figi: 3608 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self.figi)) 3609 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self.figi) 3610 3611 for instrument in portfolio["stat"]["orders"]: 3612 if instrument["figi"] == self.figi: 3613 result = True 3614 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self.figi) 3615 break 3616 3617 else: 3618 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3619 3620 uLogger.debug(msg) 3621 3622 return result
Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif limit orders list contains some limit orders for the instrument,Falseotherwise.
3624 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3625 """ 3626 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3627 Instrument must be defined by `ticker` (highly priority) or `figi`. 3628 3629 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3630 :return: list with `orderID`s of limit orders. 3631 """ 3632 result = [] 3633 msg = "Instrument not defined!" 3634 3635 if portfolio is None or not portfolio: 3636 portfolio = self.Overview(show=False) 3637 3638 if self.ticker: 3639 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self.ticker)) 3640 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self.ticker) 3641 3642 for instrument in portfolio["stat"]["orders"]: 3643 if instrument["ticker"] == self.ticker: 3644 result.append(instrument["orderID"]) 3645 3646 if result: 3647 msg = "Instrument with ticker [{}] is present in limit orders list".format(self.ticker) 3648 3649 elif self.figi: 3650 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self.figi)) 3651 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self.figi) 3652 3653 for instrument in portfolio["stat"]["orders"]: 3654 if instrument["figi"] == self.figi: 3655 result.append(instrument["orderID"]) 3656 3657 if result: 3658 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self.figi) 3659 3660 else: 3661 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3662 3663 uLogger.debug(msg) 3664 3665 return result
Returns list with all orderIDs of opened pending limit orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of limit orders.
3667 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3668 """ 3669 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3670 3671 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3672 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3673 """ 3674 result = False 3675 msg = "Instrument not defined!" 3676 3677 if portfolio is None or not portfolio: 3678 portfolio = self.Overview(show=False) 3679 3680 if self.ticker: 3681 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self.ticker)) 3682 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self.ticker) 3683 3684 for instrument in portfolio["stat"]["stopOrders"]: 3685 if instrument["ticker"] == self.ticker: 3686 result = True 3687 msg = "Instrument with ticker [{}] is present in stop orders list".format(self.ticker) 3688 break 3689 3690 elif self.figi: 3691 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self.figi)) 3692 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self.figi) 3693 3694 for instrument in portfolio["stat"]["stopOrders"]: 3695 if instrument["figi"] == self.figi: 3696 result = True 3697 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self.figi) 3698 break 3699 3700 else: 3701 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3702 3703 uLogger.debug(msg) 3704 3705 return result
Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif stop orders list contains some stop orders for the instrument,Falseotherwise.
3707 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3708 """ 3709 Returns list with all `orderID`s of opened stop orders for the instrument. 3710 Instrument must be defined by `ticker` (highly priority) or `figi`. 3711 3712 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3713 :return: list with `orderID`s of stop orders. 3714 """ 3715 result = [] 3716 msg = "Instrument not defined!" 3717 3718 if portfolio is None or not portfolio: 3719 portfolio = self.Overview(show=False) 3720 3721 if self.ticker: 3722 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self.ticker)) 3723 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self.ticker) 3724 3725 for instrument in portfolio["stat"]["stopOrders"]: 3726 if instrument["ticker"] == self.ticker: 3727 result.append(instrument["orderID"]) 3728 3729 if result: 3730 msg = "Instrument with ticker [{}] is present in stop orders list".format(self.ticker) 3731 3732 elif self.figi: 3733 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self.figi)) 3734 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self.figi) 3735 3736 for instrument in portfolio["stat"]["stopOrders"]: 3737 if instrument["figi"] == self.figi: 3738 result.append(instrument["orderID"]) 3739 3740 if result: 3741 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self.figi) 3742 3743 else: 3744 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3745 3746 uLogger.debug(msg) 3747 3748 return result
Returns list with all orderIDs of opened stop orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of stop orders.
3750 def RequestLimits(self) -> dict: 3751 """ 3752 Method for obtaining the available funds for withdrawal for current `accountId`. 3753 3754 See also: 3755 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3756 - `OverviewLimits()` method 3757 3758 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3759 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3760 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3761 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3762 """ 3763 if self.accountId is None or not self.accountId: 3764 uLogger.error("Variable `accountId` must be defined for using this method!") 3765 raise Exception("Account ID required") 3766 3767 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3768 3769 self.body = str({"accountId": self.accountId}) 3770 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3771 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3772 3773 if self.moreDebug: 3774 uLogger.debug("Records about available funds for withdrawal successfully received") 3775 3776 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3778 def OverviewLimits(self, show: bool = False) -> dict: 3779 """ 3780 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3781 3782 See also: `RequestLimits()`. 3783 3784 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3785 :return: dict with raw parsed data from server and some calculated statistics about it. 3786 """ 3787 if self.accountId is None or not self.accountId: 3788 uLogger.error("Variable `accountId` must be defined for using this method!") 3789 raise Exception("Account ID required") 3790 3791 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3792 3793 view = { 3794 "rawLimits": rawLimits, 3795 "limits": { # parsed data for every currency: 3796 "money": { # this is an array of portfolio currency positions 3797 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3798 }, 3799 "blocked": { # this is an array of blocked currency 3800 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3801 }, 3802 "blockedGuarantee": { # this is locked money under collateral for futures 3803 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3804 }, 3805 }, 3806 } 3807 3808 # --- Prepare text table with limits in human-readable format: 3809 if show: 3810 info = [ 3811 "# Withdrawal limits\n\n", 3812 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3813 "* **Account ID:** [{}]\n".format(self.accountId), 3814 ] 3815 3816 if view["limits"]["money"]: 3817 info.extend([ 3818 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3819 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3820 ]) 3821 3822 else: 3823 info.append("\nNo withdrawal limits\n") 3824 3825 for curr in view["limits"]["money"].keys(): 3826 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3827 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3828 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3829 3830 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3831 "[{}]".format(curr), 3832 "{:.2f}".format(view["limits"]["money"][curr]), 3833 "{:.2f}".format(availableMoney), 3834 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3835 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3836 ) 3837 3838 if curr == "rub": 3839 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3840 3841 else: 3842 info.append(infoStr) 3843 3844 infoText = "".join(info) 3845 3846 uLogger.info(infoText) 3847 3848 if self.withdrawalLimitsFile: 3849 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3850 fH.write(infoText) 3851 3852 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3853 3854 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3856 def RequestAccounts(self) -> dict: 3857 """ 3858 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3859 3860 See also: 3861 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3862 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3863 - `OverviewUserInfo()` method 3864 3865 :return: dict with raw data from server that contains accounts info. Example of dict: 3866 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3867 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3868 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3869 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3870 """ 3871 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3872 3873 self.body = str({}) 3874 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3875 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3876 3877 if self.moreDebug: 3878 uLogger.debug("Records about available accounts successfully received") 3879 3880 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
3882 def RequestUserInfo(self) -> dict: 3883 """ 3884 Method for requesting common user's information. 3885 3886 See also: 3887 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3888 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3889 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3890 - `OverviewUserInfo()` method 3891 3892 :return: dict with raw data from server that contains user's information. Example of dict: 3893 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3894 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3895 """ 3896 uLogger.debug("Requesting common user's information. Wait, please...") 3897 3898 self.body = str({}) 3899 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3900 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3901 3902 if self.moreDebug: 3903 uLogger.debug("Records about current user successfully received") 3904 3905 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
3907 def RequestMarginStatus(self, accountId: str = None) -> dict: 3908 """ 3909 Method for requesting margin calculation for defined account ID. 3910 3911 See also: 3912 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3913 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3914 - `OverviewUserInfo()` method 3915 3916 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3917 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3918 Example of responses: 3919 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3920 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3921 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3922 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3923 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3924 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3925 """ 3926 if accountId is None or not accountId: 3927 if self.accountId is None or not self.accountId: 3928 uLogger.error("Variable `accountId` must be defined for using this method!") 3929 raise Exception("Account ID required") 3930 3931 else: 3932 accountId = self.accountId # use `self.accountId` (main ID) by default 3933 3934 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3935 3936 self.body = str({"accountId": accountId}) 3937 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3938 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3939 3940 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3941 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3942 rawMargin = {} 3943 3944 else: 3945 if self.moreDebug: 3946 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3947 3948 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
3950 def RequestTariffLimits(self) -> dict: 3951 """ 3952 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3953 3954 See also: 3955 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3956 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3957 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3958 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3959 - `OverviewUserInfo()` method 3960 3961 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3962 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3963 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3964 """ 3965 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3966 3967 self.body = str({}) 3968 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3969 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3970 3971 if self.moreDebug: 3972 uLogger.debug("Records with limits of current tariff successfully received") 3973 3974 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
3976 def RequestBondCoupons(self, iJSON: dict) -> dict: 3977 """ 3978 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3979 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3980 All dates are in UTC timezone. 3981 3982 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3983 Documentation: 3984 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3985 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3986 3987 See also: `ExtendBondsData()`. 3988 3989 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3990 If raw iJSON is not data of bond then server returns an error [400] with message: 3991 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3992 :return: dictionary with bond payment calendar. Response example 3993 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3994 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3995 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3996 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3997 """ 3998 if iJSON["figi"] is None or not iJSON["figi"]: 3999 uLogger.error("FIGI must be defined for using this method!") 4000 raise Exception("FIGI required") 4001 4002 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4003 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4004 4005 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4006 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4007 self.figi, 4008 startDate, 4009 endDate, 4010 )) 4011 4012 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4013 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4014 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4015 4016 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4017 uLogger.warning("Instrument type is not bond!") 4018 4019 else: 4020 if self.moreDebug: 4021 uLogger.debug("Records about bond payment calendar successfully received") 4022 4023 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self.ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
4025 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4026 """ 4027 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4028 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4029 coupon yields, current yields and some statistics etc. 4030 4031 WARNING! This is too long operation if a lot of bonds requested from broker server. 4032 4033 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4034 4035 :param instruments: list of strings with tickers or FIGIs. 4036 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4037 for further used by data scientists or stock analytics. 4038 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4039 In XLSX-file and Pandas DataFrame fields mean: 4040 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4041 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4042 """ 4043 if instruments is None or not instruments: 4044 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4045 raise Exception("Ticker or FIGI required") 4046 4047 if isinstance(instruments, str): 4048 instruments = [instruments] 4049 4050 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4051 4052 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4053 4054 iCount = len(uniqueInstruments) 4055 tooLong = iCount >= 20 4056 if tooLong: 4057 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4058 4059 bonds = None 4060 for i, self.figi in enumerate(uniqueInstruments): 4061 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4062 4063 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4064 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4065 rawBond = self.SearchByFIGI(requestPrice=True) 4066 4067 # Widen raw data with UTC current time (iData["actualDateTime"]): 4068 actualDate = datetime.now(tzutc()) 4069 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4070 4071 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4072 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4073 4074 # Replace some values with human-readable: 4075 iData["nominalCurrency"] = iData["nominal"]["currency"] 4076 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4077 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4078 iData["aciCurrency"] = iData["aciValue"]["currency"] 4079 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4080 iData["issueSize"] = int(iData["issueSize"]) 4081 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4082 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4083 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4084 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4085 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4086 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4087 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4088 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4089 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4090 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4091 4092 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4093 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4094 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4095 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4096 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4097 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4098 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4099 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4100 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4101 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4102 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4103 4104 # Widen raw data with calendar data from `rawCalendar` values: 4105 calendarData = [] 4106 if "events" in iData["rawCalendar"].keys(): 4107 for item in iData["rawCalendar"]["events"]: 4108 calendarData.append({ 4109 "couponDate": item["couponDate"], 4110 "couponNumber": int(item["couponNumber"]), 4111 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4112 "payCurrency": item["payOneBond"]["currency"], 4113 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4114 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4115 "couponStartDate": item["couponStartDate"], 4116 "couponEndDate": item["couponEndDate"], 4117 "couponPeriod": item["couponPeriod"], 4118 }) 4119 4120 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4121 if "maturityDate" not in iData.keys(): 4122 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4123 4124 # Widen raw data with Coupon Rate. 4125 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4126 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4127 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4128 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4129 4130 # Widen raw data with Yield to Maturity (YTM) on current date. 4131 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4132 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4133 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4134 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4135 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4136 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4137 4138 iData["calendar"] = calendarData # adds calendar at the end 4139 4140 # Remove not used data: 4141 iData.pop("uid") 4142 iData.pop("positionUid") 4143 iData.pop("currentPrice") 4144 iData.pop("rawCalendar") 4145 4146 colNames = list(iData.keys()) 4147 if bonds is None: 4148 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4149 4150 else: 4151 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4152 4153 else: 4154 uLogger.warning("Instrument is not a bond!") 4155 4156 processed = round(100 * (i + 1) / iCount, 1) 4157 if tooLong and processed % 5 == 0: 4158 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4159 4160 else: 4161 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4162 4163 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4164 4165 # Saving bonds from Pandas DataFrame to XLSX sheet: 4166 if xlsx and self.bondsXLSXFile: 4167 with pd.ExcelWriter( 4168 path=self.bondsXLSXFile, 4169 date_format=TKS_DATE_FORMAT, 4170 datetime_format=TKS_DATE_TIME_FORMAT, 4171 mode="w", 4172 ) as writer: 4173 bonds.to_excel( 4174 writer, 4175 sheet_name="Extended bonds data", 4176 index=True, 4177 encoding="UTF-8", 4178 freeze_panes=(1, 1), 4179 ) # saving as XLSX-file with freeze first row and column as headers 4180 4181 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4182 4183 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4185 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4186 """ 4187 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4188 4189 WARNING! This is too long operation if a lot of bonds requested from broker server. 4190 4191 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4192 4193 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4194 extended information about bonds: main info, current prices, bond payment calendar, 4195 coupon yields, current yields and some statistics etc. 4196 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4197 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4198 for further used by data scientists or stock analytics. 4199 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4200 """ 4201 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4202 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4203 4204 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4205 4206 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4207 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4208 calendar = None 4209 for bond in extBonds.iterrows(): 4210 for item in bond[1]["calendar"]: 4211 cData = { 4212 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4213 "couponDate": item["couponDate"], 4214 "figi": bond[1]["figi"], 4215 "ticker": bond[1]["ticker"], 4216 "name": bond[1]["name"], 4217 "couponNumber": item["couponNumber"], 4218 "payOneBond": item["payOneBond"], 4219 "payCurrency": item["payCurrency"], 4220 "couponType": item["couponType"], 4221 "couponPeriod": item["couponPeriod"], 4222 "fixDate": item["fixDate"], 4223 "couponStartDate": item["couponStartDate"], 4224 "couponEndDate": item["couponEndDate"], 4225 } 4226 4227 if calendar is None: 4228 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4229 4230 else: 4231 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4232 4233 if calendar is not None: 4234 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4235 4236 # Saving calendar from Pandas DataFrame to XLSX sheet: 4237 if xlsx: 4238 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4239 4240 with pd.ExcelWriter( 4241 path=xlsxCalendarFile, 4242 date_format=TKS_DATE_FORMAT, 4243 datetime_format=TKS_DATE_TIME_FORMAT, 4244 mode="w", 4245 ) as writer: 4246 humanReadable = calendar.copy(deep=True) 4247 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4248 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4249 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4250 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4251 humanReadable.columns = colNames # human-readable column names 4252 4253 humanReadable.to_excel( 4254 writer, 4255 sheet_name="Bond payments calendar", 4256 index=False, 4257 encoding="UTF-8", 4258 freeze_panes=(1, 2), 4259 ) # saving as XLSX-file with freeze first row and column as headers 4260 4261 del humanReadable # release df in memory 4262 4263 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4264 4265 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4267 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4268 """ 4269 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4270 Also, creates Markdown file with calendar data, `calendar.md` by default. 4271 4272 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4273 4274 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4275 extended information about bonds: main info, current prices, bond payment calendar, 4276 coupon yields, current yields and some statistics etc. 4277 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4278 :param show: if `True` then also printing bonds payment calendar to the console, 4279 otherwise save to file `calendarFile` only. `False` by default. 4280 :return: multilines text in Markdown format with bonds payment calendar as a table. 4281 """ 4282 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4283 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4284 4285 infoText = "# Bond payments calendar\n\n" 4286 4287 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4288 4289 if not (calendar is None or calendar.empty): 4290 splitLine = "| | | | | | | | | |\n" 4291 4292 info = [ 4293 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4294 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4295 ] 4296 4297 newMonth = False 4298 notOneBond = calendar["figi"].nunique() > 1 4299 for i, bond in enumerate(calendar.iterrows()): 4300 if newMonth and notOneBond: 4301 info.append(splitLine) 4302 4303 info.append( 4304 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4305 " √" if bond[1]["paid"] else " —", 4306 bond[1]["couponDate"].split("T")[0], 4307 bond[1]["figi"], 4308 bond[1]["ticker"], 4309 bond[1]["couponNumber"], 4310 "{} {}".format( 4311 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4312 bond[1]["payCurrency"], 4313 ), 4314 bond[1]["couponType"], 4315 bond[1]["couponPeriod"], 4316 bond[1]["fixDate"].split("T")[0], 4317 ) 4318 ) 4319 4320 if i < len(calendar.values) - 1: 4321 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4322 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4323 newMonth = False if curDate.month == nextDate.month else True 4324 4325 else: 4326 newMonth = False 4327 4328 infoText += "".join(info) 4329 4330 if show: 4331 uLogger.info("{}".format(infoText)) 4332 4333 if self.calendarFile is not None: 4334 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4335 fH.write(infoText) 4336 4337 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4338 4339 else: 4340 infoText += "No data\n" 4341 4342 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4344 def OverviewAccounts(self, show: bool = False) -> dict: 4345 """ 4346 Method for parsing and show simple table with all available user accounts. 4347 4348 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4349 4350 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4351 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4352 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4353 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4354 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4355 "closed": "—", "access": "Full access" }, ...}}` 4356 """ 4357 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4358 4359 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4360 accounts = { 4361 item["id"]: { 4362 "type": TKS_ACCOUNT_TYPES[item["type"]], 4363 "name": item["name"], 4364 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4365 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4366 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4367 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4368 } for item in rawAccounts["accounts"] 4369 } 4370 4371 # Raw and parsed data with some fields replaced in "stat" section: 4372 view = { 4373 "rawAccounts": rawAccounts, 4374 "stat": accounts, 4375 } 4376 4377 # --- Prepare simple text table with only accounts data in human-readable format: 4378 if show: 4379 info = [ 4380 "# User accounts\n\n", 4381 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4382 "| Account ID | Type | Status | Name |\n", 4383 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4384 ] 4385 4386 for account in view["stat"].keys(): 4387 info.extend([ 4388 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4389 account, 4390 view["stat"][account]["type"], 4391 view["stat"][account]["status"], 4392 view["stat"][account]["name"], 4393 ) 4394 ]) 4395 4396 infoText = "".join(info) 4397 4398 uLogger.info(infoText) 4399 4400 if self.userAccountsFile: 4401 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4402 fH.write(infoText) 4403 4404 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4405 4406 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4408 def OverviewUserInfo(self, show: bool = False) -> dict: 4409 """ 4410 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4411 4412 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4413 4414 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4415 :return: dict with raw parsed data from server and some calculated statistics about it. 4416 """ 4417 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4418 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4419 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4420 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4421 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4422 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4423 4424 # This is dict with parsed common user data: 4425 userInfo = { 4426 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4427 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4428 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4429 "tariff": rawUserInfo["tariff"], 4430 } 4431 4432 # This is an array of dict with parsed margin statuses for every account IDs: 4433 margins = {} 4434 for accountId in accounts.keys(): 4435 if rawMargins[accountId]: 4436 margins[accountId] = { 4437 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4438 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4439 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4440 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4441 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4442 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4443 } 4444 4445 else: 4446 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4447 4448 unary = {} # unary-connection limits 4449 for item in rawTariffLimits["unaryLimits"]: 4450 if item["limitPerMinute"] in unary.keys(): 4451 unary[item["limitPerMinute"]].extend(item["methods"]) 4452 4453 else: 4454 unary[item["limitPerMinute"]] = item["methods"] 4455 4456 stream = {} # stream-connection limits 4457 for item in rawTariffLimits["streamLimits"]: 4458 if item["limit"] in stream.keys(): 4459 stream[item["limit"]].extend(item["streams"]) 4460 4461 else: 4462 stream[item["limit"]] = item["streams"] 4463 4464 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4465 limits = { 4466 "unary": unary, 4467 "stream": stream, 4468 } 4469 4470 # Raw and parsed data as an output result: 4471 view = { 4472 "rawUserInfo": rawUserInfo, 4473 "rawAccounts": rawAccounts, 4474 "rawMargins": rawMargins, 4475 "rawTariffLimits": rawTariffLimits, 4476 "stat": { 4477 "userInfo": userInfo, 4478 "accounts": accounts, 4479 "margins": margins, 4480 "limits": limits, 4481 }, 4482 } 4483 4484 # --- Prepare text table with user information in human-readable format: 4485 if show: 4486 info = [ 4487 "# Full user information\n\n", 4488 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4489 "## Common information\n\n", 4490 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4491 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4492 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4493 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4494 "\n## User accounts\n\n", 4495 ] 4496 4497 for account in view["stat"]["accounts"].keys(): 4498 info.extend([ 4499 "### ID: [{}]\n\n".format(account), 4500 "| Parameters | Values |\n", 4501 "|----------------------|--------------------------------------------------------------|\n", 4502 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4503 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4504 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4505 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4506 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4507 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4508 ]) 4509 4510 if margins[account]: 4511 info.extend([ 4512 "| Margin status: | Enabled |\n", 4513 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4514 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4515 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4516 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4517 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4518 ]) 4519 4520 else: 4521 info.append("| Margin status: | Disabled |\n\n") 4522 4523 info.extend([ 4524 "\n## Current user tariff limits\n", 4525 "\nSee also:\n", 4526 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4527 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4528 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4529 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4530 "\n### Unary limits\n", 4531 ]) 4532 4533 if unary: 4534 for key, values in sorted(unary.items()): 4535 info.append("\n* Max requests per minute: {}\n".format(key)) 4536 4537 for value in values: 4538 info.append(" - {}\n".format(value)) 4539 4540 else: 4541 info.append("\nNot available\n") 4542 4543 info.append("\n### Stream limits\n") 4544 4545 if stream: 4546 for key, values in sorted(stream.items()): 4547 info.append("\n* Max stream connections: {}\n".format(key)) 4548 4549 for value in values: 4550 info.append(" - {}\n".format(value)) 4551 4552 else: 4553 info.append("\nNot available\n") 4554 4555 infoText = "".join(info) 4556 4557 uLogger.info(infoText) 4558 4559 if self.userInfoFile: 4560 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4561 fH.write(infoText) 4562 4563 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4564 4565 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4568class Args: 4569 """ 4570 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4571 """ 4572 def __init__(self, **kwargs): 4573 self.__dict__.update(kwargs) 4574 4575 def __getattr__(self, item): 4576 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4579def ParseArgs(): 4580 """This function get and parse command line keys.""" 4581 parser = ArgumentParser() # command-line string parser 4582 4583 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4584 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4585 4586 # --- options: 4587 4588 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4589 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4590 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4591 4592 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4593 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4594 4595 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4596 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4597 4598 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4599 4600 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4601 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4602 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4603 4604 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4605 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4606 4607 # --- commands: 4608 4609 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4610 4611 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4612 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4613 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4614 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4615 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4616 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4617 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4618 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4619 4620 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4621 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4622 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4623 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4624 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4625 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4626 4627 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4628 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4629 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4630 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4631 4632 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4633 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4634 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4635 4636 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4637 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4638 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4639 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4640 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4641 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4642 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4643 4644 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4645 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4646 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4647 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4648 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4649 4650 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4651 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4652 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4653 4654 cmdArgs = parser.parse_args() 4655 return cmdArgs
This function get and parse command line keys.
4658def Main(**kwargs): 4659 """ 4660 Main function for work with TKSBrokerAPI in the console. 4661 4662 See examples: 4663 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4664 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4665 """ 4666 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4667 4668 if args.debug_level: 4669 uLogger.level = 10 # always debug level by default 4670 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4671 4672 exitCode = 0 4673 start = datetime.now(tzutc()) 4674 uLogger.debug("=-" * 50) 4675 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4676 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4677 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4678 )) 4679 4680 # trying to calculate full current version: 4681 buildVersion = __version__ 4682 try: 4683 v = version("tksbrokerapi") 4684 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4685 4686 except Exception: 4687 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4688 4689 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4690 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4691 4692 try: 4693 if args.version: 4694 print("TKSBrokerAPI {}".format(buildVersion)) 4695 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4696 4697 else: 4698 # Init class for trading with Tinkoff Broker: 4699 trader = TinkoffBrokerServer( 4700 token=args.token, 4701 accountId=args.account_id, 4702 useCache=not args.no_cache, 4703 ) 4704 4705 # --- set some options: 4706 4707 if args.more: 4708 trader.moreDebug = True 4709 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4710 4711 if args.ticker: 4712 ticker = args.ticker.upper() # Tickers may be upper case only 4713 4714 if ticker in trader.aliasesKeys: 4715 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4716 4717 else: 4718 trader.ticker = ticker 4719 4720 if args.figi: 4721 trader.figi = args.figi.upper() # FIGIs may be upper case only 4722 4723 if args.depth is not None: 4724 trader.depth = args.depth 4725 4726 # --- do one command: 4727 4728 if args.list: 4729 if args.output is not None: 4730 trader.instrumentsFile = args.output 4731 4732 trader.ShowInstrumentsInfo(show=True) 4733 4734 elif args.list_xlsx: 4735 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4736 4737 elif args.bonds_xlsx is not None: 4738 if args.output is not None: 4739 trader.bondsXLSXFile = args.output 4740 4741 if len(args.bonds_xlsx) == 0: 4742 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4743 4744 else: 4745 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4746 4747 elif args.search: 4748 if args.output is not None: 4749 trader.searchResultsFile = args.output 4750 4751 trader.SearchInstruments(pattern=args.search[0], show=True) 4752 4753 elif args.info: 4754 if not (args.ticker or args.figi): 4755 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4756 raise Exception("Ticker or FIGI required") 4757 4758 if args.output is not None: 4759 trader.infoFile = args.output 4760 4761 if args.ticker: 4762 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4763 4764 else: 4765 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4766 4767 elif args.calendar is not None: 4768 if args.output is not None: 4769 trader.calendarFile = args.output 4770 4771 if len(args.calendar) == 0: 4772 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4773 4774 else: 4775 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4776 4777 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4778 4779 elif args.price: 4780 if not (args.ticker or args.figi): 4781 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4782 raise Exception("Ticker or FIGI required") 4783 4784 trader.GetCurrentPrices(show=True) 4785 4786 elif args.prices is not None: 4787 if args.output is not None: 4788 trader.pricesFile = args.output 4789 4790 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4791 4792 elif args.overview: 4793 if args.output is not None: 4794 trader.overviewFile = args.output 4795 4796 trader.Overview(show=True, details="full") 4797 4798 elif args.overview_digest: 4799 if args.output is not None: 4800 trader.overviewDigestFile = args.output 4801 4802 trader.Overview(show=True, details="digest") 4803 4804 elif args.overview_positions: 4805 if args.output is not None: 4806 trader.overviewPositionsFile = args.output 4807 4808 trader.Overview(show=True, details="positions") 4809 4810 elif args.overview_orders: 4811 if args.output is not None: 4812 trader.overviewOrdersFile = args.output 4813 4814 trader.Overview(show=True, details="orders") 4815 4816 elif args.overview_analytics: 4817 if args.output is not None: 4818 trader.overviewAnalyticsFile = args.output 4819 4820 trader.Overview(show=True, details="analytics") 4821 4822 elif args.overview_calendar: 4823 if args.output is not None: 4824 trader.overviewAnalyticsFile = args.output 4825 4826 trader.Overview(show=True, details="calendar") 4827 4828 elif args.deals is not None: 4829 if args.output is not None: 4830 trader.reportFile = args.output 4831 4832 if 0 <= len(args.deals) < 3: 4833 trader.Deals( 4834 start=args.deals[0] if len(args.deals) >= 1 else None, 4835 end=args.deals[1] if len(args.deals) == 2 else None, 4836 show=True, # Always show deals report in console 4837 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4838 ) 4839 4840 else: 4841 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4842 raise Exception("Incorrect value") 4843 4844 elif args.history is not None: 4845 if args.output is not None: 4846 trader.historyFile = args.output 4847 4848 if 0 <= len(args.history) < 3: 4849 dataReceived = trader.History( 4850 start=args.history[0] if len(args.history) >= 1 else None, 4851 end=args.history[1] if len(args.history) == 2 else None, 4852 interval="hour" if args.interval is None or not args.interval else args.interval, 4853 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4854 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4855 show=True, # shows all downloaded candles in console 4856 ) 4857 4858 if args.render_chart is not None and dataReceived is not None: 4859 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4860 4861 trader.ShowHistoryChart( 4862 candles=dataReceived, 4863 interact=iChart, 4864 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4865 ) 4866 4867 else: 4868 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4869 raise Exception("Incorrect value") 4870 4871 elif args.load_history is not None: 4872 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4873 4874 if args.render_chart is not None and histData is not None: 4875 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4876 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4877 4878 trader.ShowHistoryChart( 4879 candles=histData, 4880 interact=iChart, 4881 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4882 ) 4883 4884 elif args.trade is not None: 4885 if 1 <= len(args.trade) <= 5: 4886 trader.Trade( 4887 operation=args.trade[0], 4888 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4889 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4890 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4891 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4892 ) 4893 4894 else: 4895 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4896 4897 elif args.buy is not None: 4898 if 0 <= len(args.buy) <= 4: 4899 trader.Buy( 4900 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4901 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4902 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4903 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4904 ) 4905 4906 else: 4907 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4908 4909 elif args.sell is not None: 4910 if 0 <= len(args.sell) <= 4: 4911 trader.Sell( 4912 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4913 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4914 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4915 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4916 ) 4917 4918 else: 4919 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4920 4921 elif args.order: 4922 if 4 <= len(args.order) <= 7: 4923 trader.Order( 4924 operation=args.order[0], 4925 orderType=args.order[1], 4926 lots=int(args.order[2]), 4927 targetPrice=float(args.order[3]), 4928 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4929 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4930 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4931 ) 4932 4933 else: 4934 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4935 4936 elif args.buy_limit: 4937 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4938 4939 elif args.sell_limit: 4940 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4941 4942 elif args.buy_stop: 4943 if 2 <= len(args.buy_stop) <= 7: 4944 trader.BuyStop( 4945 lots=int(args.buy_stop[0]), 4946 targetPrice=float(args.buy_stop[1]), 4947 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4948 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4949 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4950 ) 4951 4952 else: 4953 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4954 4955 elif args.sell_stop: 4956 if 2 <= len(args.sell_stop) <= 7: 4957 trader.SellStop( 4958 lots=int(args.sell_stop[0]), 4959 targetPrice=float(args.sell_stop[1]), 4960 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4961 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4962 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4963 ) 4964 4965 else: 4966 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4967 4968 # elif args.buy_order_grid is not None: 4969 # # update order grid work with api v2 4970 # if len(args.buy_order_grid) == 2: 4971 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4972 # 4973 # for order in orderParams: 4974 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4975 # 4976 # else: 4977 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4978 # 4979 # elif args.sell_order_grid is not None: 4980 # # update order grid work with api v2 4981 # if len(args.sell_order_grid) >= 2: 4982 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4983 # 4984 # for order in orderParams: 4985 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4986 # 4987 # else: 4988 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4989 4990 elif args.close_order is not None: 4991 trader.CloseOrders(args.close_order) # close only one order 4992 4993 elif args.close_orders is not None: 4994 trader.CloseOrders(args.close_orders) # close list of orders 4995 4996 elif args.close_trade: 4997 if not (args.ticker or args.figi): 4998 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4999 raise Exception("Ticker or FIGI required") 5000 5001 if args.ticker: 5002 trader.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 5003 5004 else: 5005 trader.CloseTrades([args.figi]) # close only one trade by FIGI 5006 5007 elif args.close_trades is not None: 5008 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5009 5010 elif args.close_all is not None: 5011 if args.ticker: 5012 trader.CloseAllByTicker(instrument=args.ticker) 5013 5014 elif args.figi: 5015 trader.CloseAllByFIGI(instrument=args.figi) 5016 5017 else: 5018 trader.CloseAll(*args.close_all) 5019 5020 elif args.limits: 5021 if args.output is not None: 5022 trader.withdrawalLimitsFile = args.output 5023 5024 trader.OverviewLimits(show=True) 5025 5026 elif args.user_info: 5027 if args.output is not None: 5028 trader.userInfoFile = args.output 5029 5030 trader.OverviewUserInfo(show=True) 5031 5032 elif args.account: 5033 if args.output is not None: 5034 trader.userAccountsFile = args.output 5035 5036 trader.OverviewAccounts(show=True) 5037 5038 else: 5039 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5040 raise Exception("There is no command to execute") 5041 5042 except Exception: 5043 trace = tb.format_exc() 5044 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5045 if e in trace: 5046 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5047 break 5048 5049 uLogger.debug(trace) 5050 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5051 exitCode = 255 # an error occurred, must be open a ticket for this issue 5052 5053 finally: 5054 finish = datetime.now(tzutc()) 5055 5056 if exitCode == 0: 5057 if args.more: 5058 uLogger.debug("All operations were finished success (summary code is 0).") 5059 5060 else: 5061 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5062 os.path.abspath(uLog.defaultLogFile), exitCode, 5063 )) 5064 5065 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5066 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5067 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5068 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5069 )) 5070 uLogger.debug("=-" * 50) 5071 5072 if not kwargs: 5073 sys.exit(exitCode) 5074 5075 else: 5076 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: